Eidgenössische Technische Hochschule Zürich Ecole polytechnique fédérale de Zurich Politecnico federale di Zurigo Federal Institute of Technology at Zurich Institut für Theoretische Informatik Peter Widmayer Michael Gatto Datenstrukturen & Algorithmen Lösungen 8 SS 07 Aufgabe 8.1: a) Eine Multipop-Operation auf einem Stack mit n Elemente hat natürlich eine worst-case Laufzeit von O(n): falls wir das Element nicht finden, oder wir das unterste Element im Stack suchen, so entfernen wir alle n Elemente aus dem Stack, was eine Laufzeit von O(n) in Anspruch nimmt. b) Die Idee ist, beim Einfügen eines Elements gleich für dessen Entfernen aus dem Stack zu bezahlen. Also legen wir je 1 Franken bei jeder Einfügeoperation auf die Seite. Bei einer Multipop-Operation können wir diesen Kredit benutzen, um das Entfernen des Elements zu bezahlen. c) Ein formaler Beweis sieht wie folgt aus: Wir betrachten eine Folge von n Operationen. Als Kontostand Bal` nach der `-ten Operation definieren wir die Anzahl Elemente, die auf dem Stack liegen. Die amortisierten Kosten sind definiert als a` = − Bal`−1 , die tatsächlichen Kosten t` plus die Ptn` + Bal` P Pn n Differenz der Kontostände. Es soll gelten: `=1 a` = `=1 (t` + Bal` − Bal`−1 ) = `=1 t` + Baln − Bal0 , wobei Baln − Bal0 ≥ 0 da wir die tatsächlichen Kosten durch die amortisierten Kosten decken müssen. Die amortisierten Kosten für eine Top-Operation sind a` = t` + Bal` − Bal`−1 = 1, da sich die Anzahl Elemente auf dem Stack nicht ändert und wir das oberste Element in konstanter Zeit anschauen können. Sei die `-te Operation eine Push-Operation, mit bereits m − 1 Elementen auf dem Stack. Es gilt für die amortisierten Kosten: a` = t` +Bal` −Bal`−1 = 1+m−(m−1) = 2, die amortisierte Kosten sind konstant. Sei die `-te Operation eine Pop-Operation auf einem Stack mit n > 0 Elementen. Es gilt: a` = t` + Bal` − Bal`−1 = 1 + m − 1 − m = 0, die Kosten sind amortisiert konstant (sogar gratis!). Für eine Pop-Operation auf dem leeren Stack gilt: a` = t` + Bal` − Bal`−1 = 1 + 0 − 0 = 1, die Kosten sind dennoch amortisiert konstant. Der interessante Fall ist natürlich Multipop. Nehmen wir deshalb an, die `-te Operation sei eine Multipop-Operation auf einem Stack mit m Elementen, und es würden k ≤ m Elemente aus dem Stack entfernt (mehr können es ja nicht sein, da Multipop beim leeren Stack aufhört). Es gilt: a` = t` + Bal` − Bal`−1 = k + m − k − m = 0, die Kosten sind für jedes k amortisiert konstant. Es bleibt noch zu zeigen, dass Baln − Bal0 ≥ 0. Da wir mit dem leeren Stack beginnen, gilt Bal0 = 0. Weiter ist der Stack Fall leer, und es gilt Baln ≥ 0. Damit haben wir Pnim schlimmsten Pn bewiesen, dass die Ungleichung `=1 a` ≥ l=1 t` + Baln − Bal0 gilt mit konstanten amortisierten Kosten. Aufgabe 8.2: Wir zeigen, wie man eine Triangulierung benutzen kann, um eine Menge von n Zahlen zu sortieren. Gegeben sei eine Menge von n Zahlen X = {x1 , . . . , xn }. Wir bilden damit eine Instanz zu einem Triangulierungsproblem, dessen Lösung die sortierte Reihenfolge der Zahlen ablesen lässt. Die Instanz sieht wie folgt aus, und ist für die Zahlenmenge {6, 2, 5, 7} beispielhaft in der Figur unten abgebildet: jede Zahl xi ∈ X wird zu einem Punkt (xi , 0) abgebildet. Dadurch entsteht eine Menge von n Punkten, die auf einer Geraden liegen. Wir addieren noch den Punkt (x1 , 1) zu dieser Punktmenge. Es gibt eine einzige (nichtdegenerierte) Triangulierung dieser Punktmenge: sie bildet jeweils Dreiecke mit den Punkten (xi , 0), (xi+1 , 0), (x1 , 1), i ∈ {1, . . . , n − 1}. Diese Triangulierung lässt offensichtlich die sortierte Reihenfolge ablesen: man nimmt den ersten Punkt der Liste und wählt solange den Punkt mit kleinerer x-Koordinate, bis es keinen solchen Punkt mehr gibt. Somit hat man in O(n) Zeit die kleinste Zahl gefunden. Nun traversiert man die x-Achse von links nach rechts, indem man bei jedem Punkt den Nachbarn mit grösserer x-Koordinate wählt. Den Punkt (x1 , 1) ignoriert man stets. Der Aufbau der Instanz erfolgt in linearer Zeit, ebenso das Ablesen der sortierten Reihenfolge. Die Laufzeit dieses Algorithmus ist deshalb O(n) plus die Laufzeit zur Berechnung der Triangulierung. Da man eine beliebige Schlüsselmenge nicht schneller als in Ω(n log n) sortieren kann, gilt diese untere Schranke auch für die Berechnung einer Triangulierung für eine Menge von n Punkten in der Ebene. y 1 (0, 0) x Aufgabe 8.3: a) (zum Verständnis etwas präziser als 2-3 Sätze) Wir verfolgen einen Scanline-Ansatz, der die WasserInstallation von links nach rechts traversiert. Die Scanline enthält die Regenrinnen (in einem Balancierten Suchbaum, mit Schlüssel hoehe der Rinnen) die durch die Scanline geschnitten werden. Die Haltepunkte sind die Start- und Endpunkte der Rinnen. Bei jedem Startpunkt fügen wir die Rinne in den Baum ein; falls sie zuoberst liegt (und somit die maximale Höhe hat), so muss die dem Wasser exponierte Fläche der bisherigen höchsten Rinne adaptiert werden, und man merkt sich die Stelle ab der die aktuelle Rinne die oberste geworden ist. Beim den Endpunkten der Rinnen wird die jeweilige Rinne aus dem Baum entfernt. Lag die Rinne zuoberst, so muss zuerst noch die gefangene Wassermenge adaptiert werden. Nun liegt eventuell eine andere Rinne zuoberst, und wir merken uns wieder diesen Haltepunkt als die Stelle, bei der die Rinne das Maximum geworden ist. Schlussendlich fliesst das Wasser der entfernten Rinne auf ihren kleineren unmittelbaren Nachbarn im Baum (falls vorhanden). Wir inkrementieren die Wassermenge dieser Rinne um die Menge Wasser, welche die zu entfernende Rinne gefangen hat. b) from i := rinnen.lower until i> rinnen.upper do haltepunkte.put(rinnen.item(i).links) haltepunkte.put(rinnen.item(i).rechts) end sort(haltepunkte) start_segment:= haltepunkte.item(haltepunkte.lower).links from i := haltepunkte.lower until i > haltepunkte.upper do rinne := get_rinne(haltepunkte.item(i)) if (rinne.links = haltepunkte.item(i)) then -- Startpunkt oldmax := scanline.max scanline.insert(rinne) if (scanline.max.equal(rinne) then --eingefuegte Rinne ist Maximum oldmax.wassermenge := oldmax.wassermenge + (rinne.links-start_segment)*amount start_segment := rinne.links end else --Endpunkt if(scanline.max.equal(rinne)) then --Rinne ist zuoberst rinne.wassermenge := rinne.wassermenge + (rinne.rechts-start_segment)*amount start_segment := rinne.rechts end abflussrinne:= scanline.previous(rinne) scanline.remove(rinne) abflussrinne.wassermenge:= rinne.wassermenge + abflussrinne.wassermenge --abfliessendes Wasser auf neue Maximum gezaehlt. end i:= i + 1 2 end Die Laufzeit des Ansatzes ist O(N log N ): es gibt 2N Haltepunkte, die in sortierter Reihenfolge traversiert werden müssen. Pro Haltepunkt ist der Aufwand in O(log n). Die Herstellung der sortierten Reihenfolge erfolgt mit einem optimalen Sortierverfahren in O(N log N ) Zeit. c) Wir folgen dem gleichen Ansatz wie in a), mit einigen kleinen Unterschieden. Erstens wird jedes Loch zu einem zusätzlichen Haltepunkt. Zweitens lassen wir beim Entfernen einer Rinne aus der Scanline das Wasser nicht auf die unterstehende Rinne abfliessen, sondern wir berechnen zuerst nur, wieviel Wasser die Rinnen von oben direkt abfangen. Die Wassermenge, die jede Rinne durch die Löcher anderer Rinnen auffängt wird in einem zweiten Schritt berechnet. Dazu dienen die Haltepunkte bei den Löchern: bei jedem solchen Haltepunkt merken wir uns, auf welche Rinne das Wasser abfliesst. Es handelt sich dabei um die Rinne im balancierten Baum, die den nächstkleineren Schlüssel hat. Da jede Rinne in höchstens eine andere Rinne abfliesst, entsteht dadurch ein Wald von Bäumen mit folgender Interpretation: die Wassermenge der Kinder fliesst in den Vaterknoten. Am Ende des ScanlineAlgorithmus müssen die Bäume noch per post-order traversiert werden, und die Wassermengen die jede Rinne auffängt dem Vaterknoten dazuaddiert. Somit wird jedem Knoten die Wassermenge zugewiesen, die in seinen Unterknoten direkt von oben gesammelt wurde. Das Traversieren erfolgt in O(N ) Zeit, der Aufbau des Baumes auch in Zeit O(N ). Somit kann diese Aufgabe auch in Zeit O(N log N ) berechnet werden. Aufgabe 8.4: Wir vergessen kurz die Analysis-Vorlesung, und verfolgen einen kombinatorischen Ansatz, um das Integral zu berechnen. Die Beobachtung, die zum Lösungsansatz führt, ist, dass wir das Gebiet in (zum Teil degenerierte) Trapeze unterteilen können. Für jedes Trapez kann dessen Fläche berechnet werden, und das Integral entsteht aus der Summe der Flächen der Trapeze. Ein Trapez entsteht, wenn eine neue Gerade das Minimum wird und die Funktion F beschreibt, oder wenn die minimale Gerade die x-Achse schneidet (denn da ändert sich das Vorzeichen, mit dem die Fläche fürs Integral zählt). Dies impliziert, dass man alle Punkte enumerieren muss, bei denen sich die minimale Funktion ändert. Solche Änderungen entstehen nur, falls sich Geraden kreuzen. Entsprechend müssen wir alle solche Kreuzungspunkten enumerieren. Wir verfolgen deshalb einen Scanline-Ansatz. Die Scanline geht entlang der x-Achse von links nach rechts durch das Gebiet, und enthält alle Geraden in der Reihenfolge, wie sie die Scanline schneiden (in aufsteigender y-Koordinate). Bei jedem Schnittpunkt zwischen zwei Geraden tauschen wir ihre Reihenfolge in der Scanline, fügen die neu ersichtlichen Schnittpunkte als Haltepunkte der Scanline zu, und prüfen ob sich die tiefste Gerade geändert hat. Falls dies der Fall ist, so berechnen wir (mit der aus der Geometrie bekannten Formel) die Fläche des Trapezes, das gerade beendet wurde, und addieren es zum Integral. Diese Schritte wiederholen wir solange, bis wir das rechte Ende des Intervalls erreicht haben, und somit den Wert des Integrales berechnet wurde. Wir beginnen am linkem Intervall-Ende xl , und fügen die Geraden in einen balancierten Suchbaum ein. Als Schlüssel für die i-te Gerade benutzen wir fi (xl ). Wir sammeln eine Startmenge an Haltepunkte für die Scanline. Für jede Gerade prüfen wir, ob sie die x-Achse im Intervall [xl , xr ] schneidet. Falls ja, handelt es sich um einen Haltepunkt. Weiter addieren wir als Haltepunkte die Schnittpunkte von je zwei benachbarten Geraden in der Scanline, die sich im Intervall [xl , xr ] schneiden. Nun beginnen wir den Scan: wir betrachten das aktuelle Minimum in der Scanline, und merken uns xstart := xl als den Punkt, bei dem die Gerade das Minimum wird. Wir gehen zum nächsten Haltepunkt xi : falls es sich um einen Schnittpunkt zwischen zwei Geraden handelt, so vertauschen wir die zwei Geraden in der Scanline, und prüfen ob die Schnittpunkte der neu entstandenen Nachbarn im Intervall [xi , xr ] liegen. Falls ja, addieren wir sie als Haltepunkte der Scanline. Falls in der Vertauschung die Gerade fk beteiligt war, die im Intervall [xstart , xi ] das aktuelle Minimum war, so berechnen wir die Fläche des Trapez mit Eckpunkten (xstart , 0), (xi , 0), (xi , fk (xi )) und (xstart , fk (xstart )) als Ii = 21 (xi − xstart ) · (fk (xi ) + fk (xstart )). Man beachte, dass man durch diese Formel automatisch das richtige Vorzeichen erhält, mit dem die Fläche zum Integral zählt. Danach setzen wir xstart := xi . Falls der Schnittpunkt einer Gerade fk mit der x-Achse auftritt, so ist dieser nur interessant, falls diese Gerade das aktuelle Minimum ist. Falls dies zutrifft, so berechnen wir die Fläche des degenerierten Trapez (ein Dreieck) mit Eckpunkten (xstart , 0), (xi , 0), (xi , fk (xi )) und (xstart , fk (xstart )) (ein Dreieck). 3 Beim Erreichen des rechten Intervallendes xr ist die Berechnung beendet. Zum Abspeichern der Haltepunkte können wir einen Minimum-Heap benutzen. Das Einfügen der k Haltepunkte hat insgesamt eine Laufzeit von O(k log k). Pro Haltepunkt haben wir konstanten Aufwand, und somit eine gesamte Laufzeit von O(k log k + n log n). Im schlimmsten Fall gibt es für n Geraden k = Θ(n2 ) Schnittpunkte, und dies führt zu einer Laufzeit von O(n2 log n). Mit einer einfachen Überlegung lässt sich die Anzahl relevanten Schnittpunkte auf O(n) reduzieren. Bei jedem Kreuzungspunkt ist die Gerade die “unten” war nach dem Schnittpunkt nicht mehr relevant, da sie nie mehr das Minimum werden kann. Sie kann deshalb aus der Scanline entfernt werden, und für diese Gerade müssen wir keine weitere Haltepunkte betrachten. Beim Entfernen entsteht ein neues Nachbarnpaar, das sich schneiden könnte, und wir fügen dieses Paar ein. Da wir am Anfang n Haltepunkte haben, pro entfernte Gerade höchstens einen neuen Schnittpunkt einfügen, und sonst noch die Schnittpunkte mit der x-Achse betrachten müssen, gibt es insgesamt 3n Haltepunkte (wobei einige Geraden betreffen, die bereits entfernt wurden, und deshalb ignorieren kann). Aufgabe 8.5: Wir wollen untersuchen, ob der Beweis für die lineare Laufzeit des Median-Algorithmus von Blum auch mit 3er Gruppen funktioniert. Wir gehen exakt wie in Kapitel 3.1 des Buchs vor. Die Wahl des Aufteilungselementes v als Median der Mediane sichert, dass mit Ausnahme der Gruppe, in der v selbst vorkommt und der möglicherweise vorkommenden einzigen Gruppe mit weniger als drei Elementen, jede Dreiergruppe mit mittlerem Element kleiner als v wenigstens zwei Elemente enthält, die kleiner als v sind. Das gleiche gilt für Elemente, die grösser als v sind. Also gibt es in der Ausgangsfolge wenigstens N 1 N − 2) ≥ −4 2·( 2 3 3 Elemente, die kleiner als v sind,und ebenso viele Elemente, die grösser als v sind. Daraus folgt, dass das Verfahren für höchstens 2N 3 + 4 Elemente rekursiv aufgerufen werden muss. Damit ergibt sich analog zum Buch folgende Rekursionsformel für die Laufzeit: T (N ) ≤ T N 3 +T 2 N +4 +a·N 3 (1) Nun scheitert man beim Versuch, eine Konstante c zu finden mit der Eigenschaft, dass T (N ) ≤ cN gilt für alle N ≥ N0 . Analog zum Buch haben wir die Bedingung: N 2 T (N ) ≤ c · +c· N +4 +a·N 3 3 1 2 ≤ c · N + c + c · N + 5c + a · N 3 3 = cN + 6c + aN Für keine Konstante c ist diese Laufzeit kleiner als cN wie gefordert, da der Summand aN nicht wie im Fall von 5er Gruppen kompensiert werden kann. Die Konstante a ≥ 1 ist fest vorgegeben durch die Laufzeit des Aufteilungsschritts. Wir haben gezeigt, dass ein analoger Beweis für Dreiergruppen nicht funktioniert und damit erklärt, weshalb der Algorithmus von Blum mit 5er Gruppen realisiert wurde. Die Frage, ob der Algorithmus mit 3er Gruppen immer noch linear ist, ist viel schwieriger zu beantworten und sprengt eigentlich den Rahmen von Datenstrukturen und Algorithmen. Die Schwierigkeit liegt darin, dass wir in (1) für den Aufwand nur eine obere Schranke haben. Wir müssten zeigen, 2N dass es eine worst case Sequenz von Zahlen gibt, die tatsächlich auf jeder Stufe immer mindestens Zahlen für den rekursiven Aufruf übriglässt. 3 4 Für k = 7 sieht die Situation wie folgt aus. Wieder sichert die Wahl von v als Median der Mediane, dass es höchstens zwei Gruppen gibt, die ein kleineres mittleres Element als v haben und weniger als 4 Elemente, die kleiner als v sind. Die einzigen Gruppen, die diese Eigenschaft haben, sind die Gruppe, in der v selbst vorkommt, und die einzige Gruppe mit weniger als k Elementen. Jede andere Gruppe, bei der das mittlere Element kleiner als v ist, hat somit 4 Elemente, die kleiner als v sind. Analog gilt dies für die Gruppen mit mittlerem Element als v. grösserem Es gibt deshalb in der Ausgangsfolge 4 · ( 21 N7 − 2) ≥ 2N 7 − 8 Elemente, die kleiner als v sind, und gleich viele, die grösser als v sind. 5N Die Rekursion wird deshalb auf höchstens dN − 2N 7 − 8e = d 7 + 8e Elemente aufgerufen. Wir können nun wieder die Rekursionsformel für die Laufzeit schreiben: 5 N +T N +8 +a·N T (N ) ≤ T 7 7 Wie im Buch suchen wir eine Konstante c so, dass T (N ) ≤ cN , für alle N ≥ N0 . Daraus erhält man N 5 N 5 6 T (N ) ≤ c · +c· N +8 +a·N ≤c· + c + c · N + 9c + a · N = cN + 10c + aN 7 7 7 7 7 Wir wählen nun c so, dass erhält man: 6 7 cN + 10c + aN ≤ cN,. Dazu stellen wir die Bedingung, dass c > 14a. Daraus 6 6 Nc 13 cN + 10c + aN < cN + 10c + = N c + 10c ≤ cN. 7 7 14 14 N Daraus folgt 10 < 14 , N ≥ 140. Wir haben somit bewiesen, dass für alle N > 140, und bei der Wahl von c > 14a der Aufwand linear in cN ist. Der Median-Algorithmus terminiert also auch in linearer Zeit, wenn man k = 7 wählt. 5