Begleitmaterial über Effiziente Algorithmen Friedhelm Meyer auf der Heide Kontakt Friedhelm Meyer auf der Heide Universität Paderborn Fakultät für Elektrotechnik, Informatik und Mathematik Institut für Informatik und Heinz Nixdorf Institut Fürstenallee 11 33102 Paderborn email: [email protected] Stand: Wintersemester 2008/2009, 14. Oktober 2008 Inhaltsverzeichnis 3 Inhaltsverzeichnis 1 Algorithmische Geometrie 1.1 Orthogonal Range Searching . . . . . . . . . . . . . . . . . 1.1.1 Der 1D-Fall . . . . . . . . . . . . . . . . . . . . . . 1.1.2 Der 2D-Fall . . . . . . . . . . . . . . . . . . . . . . 1.1.3 Schnellere Antwortzeiten im 2D-Fall . . . . . . . . . 1.1.4 Orthogonal Range Searching in hohen Dimensionen 1.1.5 Dynamisches Orthogonal Range Searching . . . . . 1.2 Circular Range Searching . . . . . . . . . . . . . . . . . . . 1.2.1 Spanner und weak Spanner . . . . . . . . . . . . . 1.2.2 Sektorengraphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 7 7 10 11 12 12 13 14 15 2 Realisierung von Wörterbüchern durch universelles und perfektes Hashing 2.1 Abstrakte Datentypen und Datenstrukturen . . . . . . . . . . . . . . . . . 2.2 Einfache Datenstrukturen für Wörterbücher . . . . . . . . . . . . . . . . . 2.3 Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Behandlung von Kollisionen: Hashing with Chaining . . . . . . . . 2.4.2 Universelles Hashing und Worst Case Expected Time Untersuchungen 2.4.3 Perfektes Hashing und worst case konstante Zeit für Lookups . . . . 19 19 20 21 21 22 22 24 3 Flüsse in Netzwerken 3.1 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) . . . . . 3.2 Effiziente Algorithmen zur Berechnung maximaler Flüsse . . . . . . . . . . 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus . . . . . . . . . . . . 27 28 32 34 4 Graphalgorithmen: Das All-Pair-Shortest-Path 4.1 Das All Pairs Shortest Paths Problem (APSP) . 4.2 Der Floyd-Warshall Algorithmus . . . . . . . . . 4.2.1 Berechnung Transitiver Hüllen . . . . . . 38 38 38 40 Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Entwurfsmethoden für Algorithmen 5.1 Dynamisches Programmieren . . . . . . . . . . . . 5.1.1 Problem 1: Matrizen-Kettenmultiplikation 5.1.2 Problem2: Optimale binäre Suchbäume . . 5.2 Greedy-Algorithmen . . . . . . . . . . . . . . . . 5.2.1 Bruchteil-Rucksackproblem: optimal . . . 5.2.2 Rucksackproblem: sehr schlecht . . . . . . 5.2.3 Bin Packing: nicht optimal, aber gut . . . 5.2.4 Prefixcodes nach Huffman: optimal . . . . 5.2.5 Wann sind Greedy-Algorithmen optimal? . 14. Oktober 2008, Version 0.6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 41 42 45 45 46 46 48 51 Inhaltsverzeichnis 4 6 Berechnung des Medians, k-Selektion 7 Randomisierte Algorithmen 7.1 Grundbegriffe zu probabilistischen Algorithmen . . . . . . . . . 7.1.1 Randomisierte Komplexitätsklassen . . . . . . . . . . . . 7.2 Einige grundlegende randomisierte Algorithmen . . . . . . . . . 7.2.1 Verifikation von Polynom-Identitäten und Anwendungen 7.2.2 Perfekte Matchings in Bipartite Graphen . . . . . . . . . 7.2.3 Perfekte Matchings in beliebigen Graphen . . . . . . . . 7.2.4 Effiziente Tests für “p(x) = 0” . . . . . . . . . . . . . . . 7.2.5 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . 54 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 58 61 62 62 62 63 64 65 8 Ein randomisiertes Wörterbuch: Skip-Listen 67 9 Berechnung minimaler Schnitte in Graphen 9.1 Ein sehr einfacher Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . 70 70 14. Oktober 2008, Version 0.6 5 1 Algorithmische Geometrie Die Algorithmische Geometrie beschäftigt sich mit der Entwicklung effizienter und praktikabler Algorithmen zur Lösung geometrischer Probleme und mit der Bestimmung der algorithmischen Komplexität solcher Probleme. Was sind geometrische Probleme? Beispiel 1.1 Bewegungsplanung: Ein Roboter ist in einem Raum an einem Punkt p positioniert. In diesem Raum sind Hindernisse (Maschinen, Schränke, ...). Wie berechnet man zu einem Punkt q, ob es für den Roboter einen Weg von p nach q gibt? Wie findet man einen solchen Weg? Wie einen kürzesten Weg? Geographische Datenbanken: Geographische Daten sind in einer Datenbank abgelegt, um Anfragen über verschiedenste Informationen zu beantworten, z.B.: Was ist der kürzeste Weg von Paderborn nach Lingen? Was ist die maximale Entfernung von einem Ort im Ruhrgebiet zu einem Erholungsgebiet? Geographische Datenverarbeitung: Gegeben ist eine (3-dimensionale) Szene (z.B. eine Stadt). Wie berechne ich zu einer Position x und einer Richtung t das (2-dimensionale) Bild der Szene, das ich sehe, wenn ich von x aus in Richtung t (mit Blickwinkel z.B. 45o ) schaue? Hierin sind viele Teilprobleme verborgen: - Wie beschreibe ich die Szene im Rechner? - Wie identifiziere ich meine Position in der Szene? (Point Location) - Wie berechne ich verdeckte, also nicht sichtbare Teile der Szene? (Culling) - Wie berechne ich alle Objekte der Szene, die nicht zu weit entfernt (also sichtbar) sind? (Circular Range Search) - u.v.a.m. Im Grundstudium haben einige von Ihnen eventuell schon einige Grundbegriffe der Algorithmischen Geometrie kennengelernt. Themen aus der algorithmischen Geometrie in dieser Vorlesung sind Range Searching Probleme. • Orthogonal Range Searching Verwalte eine Punkt-Menge S ⊆ Rd so, daß zu einem gegebenen achsenparallelen Quader Q ⊆ Rd schnell Q ∩ S berechnet werden kann. 14. Oktober 2008, Version 0.6 6 • Circular Range Searching Verwalte eine Punkt-Menge S ⊆ Rd so, daß zu einer gegebenen Kugel K ⊆ Rd schnell K ∩ S berechnet werden kann. • Untere Schranken für geometrische Probleme Der Component Counting Lower Bound für Berechnungsbäume. Sie sollten sich an folgende Begriffe aus dem Grundstudium (lineare Algebra, Datenstrukturen + Algorithmen) erinnern. R: Menge der reellen Zahlen. Rd : d-dimensionaler (Euklidischer) Raum; Rd := {(x1 . . . , xd ), xi ∈ R, i = 1, . . . , d} Für (x1 , . . . , xd ) ∈ Rd schreiben wir kurz x. d 1 Euklidischer Abstand: |x − y| = ( Σ (xi − yi )2 ) 2 i=1 Hyperebene: Für a ∈ Rd , b ∈ R ist H(= Ha,b ) = {x ∈ Rd , ax = b}. Halbraum: H + = {x ∈ Rd , ax > b} (offen), H̄ + = {x ∈ Rd , ax ≥ b} (abgeschlossen) in R2 : Hyperebene = ˆ Gerade 3 in R : Hyperebene = ˆ Ebene Achsenparalleler Quader: Für a1 , . . . , ad , b1 , . . . , bd ∈ R, a1 ≤ b1 , . . . , ad ≤ bd ist d Q = Π [ai , bi ] ein achsenparalleler Quader i=1 Kugel: z ∈ Rd , r ∈ R+ : K = {x ∈ Rd , |z − x| ≤ r}. Rechenmodell: RAM (Random Access Machine) → siehe Grundstudium. Wir gehen (idealisiert) davon aus, das solche Maschinen arithmetische Operationen auf reellen Zahlen exakt in einem Schritt ausführen. Range Searching (Bereichs-Suche) Ein Range Searching Problem ist durch eine Familie R von Bereichen (Ranges) R ⊆ Rd definiert. Aufgabe: Organisiere eine gegebene endliche Punktmenge S ⊆ Rd so, daß bei Eingabe R ∈ R die Menge R ∩ S schnell berechnet werden kann. Beispiel 1.2 • Orthogonal Range Searching: R ist Menge der (achsenparallelen) Quader in Rd . • Circular Range Searching: R ist Menge der Kugeln in Rd . 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 1.1 7 Orthogonal Range Searching Anwendung: Einfache Datenbankanfragen: Es sind Personen mit ihrem Alter und Gehalt gespeichert (= ˜ Punkte in R2 ). Aufgabe: Zu a1 , b1 , a2 , b2 , ∈ R, a1 ≤ a2 , b1 ≤ b2 berechne alle Personen mit Alter zwischen a1 und a2 und Gehalt zwischen b1 und b2 (R = [a1 , a2 ] × [b1 , b2 ]). 1.1.1 Der 1D-Fall Aufgabe: Organisiere S = {p1 , . . . , pn } ⊆ R so, daß für gegebenes Intervall [a, b] schnell der Schnitt [a, b] ∩ S berechnet werden kann. Algorithmus: Organisiere S in einem balancierten Suchbaum T , der p1 , . . . , pn in den Blättern enthält. In jedem inneren Knoten ist der Wert des rechtesten Blattes des unter ihm hängenden linken Teilbaums gespeichert. Beispiel 1.3 S = {3, 10, 19, 23, 30, 37, 49, 58, 62, 70, 80, 89, 100, 105} 49 23 80 37 10 3 3 19 10 19 30 23 30 62 49 37 89 58 58 89 70 62 70 80 100 100 105 Abbildung 1: Balancierter Suchbaum T für die Menge S aus Bsp. 1.3 Lemma 1.4 T kann aus S in Zeit O(n log(n)) berechnet werden. Beweis: Übung (im wesentlichen Wiederholung von Techniken aus Info C bzw. “Einführung in Datenstrukturen und Algorithmen”). 2 Wie berechnen wir nun für ein Interval [a, b] die Menge S ∩ [a, b], d.h. wie soll Algorithmus 1D-Range-Query (T [a, b]) arbeiten? 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 8 Für ein x ∈ R ist der Suchweg für x in T derjenige Weg, der in der Wurzel startet und an einem inneren Knoten y in den linken/rechten Teilbaum geht, falls x ≤ y/x > y gilt. Wir berechnen zuerst die Suchwege Pa und Pb für a und b in T . Für a = 18, b = 80 sind das im obigen Beispiel die Wege nach 19 und nach 80 . Falls Pa in a’ , Pb in b’ endet, müssen alle Werte in den Blättern von a’ bis vor b’ ausgegeben werden, sowie zusätzlich der Wert von b’ , falls b0 = b gilt (wie in unserem Beispiel der Fall). Die Blätter von a’ bis vor b’ berechnen wir wie folgt: Zuerst berechnen wir den Split-Knoten von Pa und Pb , d.h. den (von oben gesehen) letzten gemeinsamen Knoten von Pa und Pb . (Im Beispiel ist das die Wurzel. Achtung: Das muß nicht immer die Wurzel sein. Bei Anfrage [18, 37] z.B. wäre es der Knoten 23.) Für jeden inneren Knoten x auf Pa (Pb ) unterhalb des Split-Knotens, von dem aus Pa (Pb ) über den linken (rechten) Suchbaum verläuft, geben wir alle Blätter unter dem rechten (linken) Subbaum von Pa (Pb ) aus. Im Beispiel liefert P18 die Subbäume mit Wurzeln 37, 23 und das Blatt 19, P80 die mit Wurzeln 58 und 70. Im folgenden beschreiben wir in einem Pseudo-Code zuerst den Algorithmus Find-Split (T, [a, b]), der in T den Split-Knoten von Pa und Pb findet, und dann den Algorithmus 1D-Range-Query (T, [a, b]), der mit Hilfe von T zu [a, b] die Menge [a, b] ∩ S berechnet. Er benutzt dabei die (nicht näher beschriebene, simple) Prozedur Report-Subtree (r), die zu Knoten v von T die Werte der Blätter im Subbaum unter v ausgibt. right(r), left(u) bezeichnet für innere Knoten r von T den rechten bzw. linken Nachfolger. Find-Split (T, [a, b]) Output: Der Splitknoten für [a, b] v := root(T ); while v ist kein Blatt and (a > xv or b ≤ xv ) do if b ≤ xv then v := lef t(v); else v := right(v); fi od return 1D-Range-Query (T, [a, b]) Output: Alle Punkte von S in [a, b] 1. V Split := Find-Split (T, [a, b])i ; 2. if V Split ist ein Blatt then gebe den Punkt in V Split zurück, wenn er in [a, b] liegt. 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 9 3. else 4. (* Folge dem Weg Pa und gebe die Punkte in den rechten Teilbäumen zurück *) v := left (V Split); while v ist kein Blatt do if a < xv then Report-Subtree (right (v)); v := left (v); else v := right (v); fi gebe den Wert von Blatt v zurück, wenn er in [a, b] liegt. 5. (* Folge dem Weg Pb und gebe die Punkte in den linken Teilbäumen zurück*) ... Analyse Korrektheit: - Es werden nur Elemente aus S ausgegeben. - Es wird kein x < a ausgegeben, da dieses Blatt eines Subbaums ist, der unter einem linken Sohn v eines Knotens auf Pa hängt, der nicht zu Pa gehört. Für solche Knoten wird nie Report-Subtree (v) aufgerufen. - Es wird kein x > b ausgegeben, ... (analog mit Pb ) - Sei x ∈ [a, b] ∩ S, Px der Suchweg nach x. Dieser Weg verläßt entweder Pa unterhalb des Split-Knotens nach rechts, oder Pb unterhalb des Split-Knotens nach links. Sei v der erste Knoten auf Px der nicht zu Pa bzw. Pb gehört. Dann wird im Algorithmus Report-Subtree (v) aufgerufen. Hierbei wird (u.a.) x ausgegeben. Laufzeit: Aufbau von T : Zeit O(n log(n)). (z.B.: Sortiere S zu p1 < . . . < pn . n=1: T = p1 n > 1 Berechne T1 für p1 , . . . , p n2 , T2 für pn/2+1 , . . . , pn . T bekommt Wurzel pn/2 und linken/rechten Subbaum T1 /T2 .) Zeit für 1D-Range-Search (T, [a, b]) - ohne die Zeit für die Report-Subtree-Aufrufe: O(log(n)), da nur die Wege Pa und Pb sowie die direkten Nachbarn der darauf liegenden Knoten besucht werden. 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 10 - Zeit für Report-Subtree (v): O(Größe des Subbaums unter v). = O (# Blätter des Subbaums unter v) = O (Outputgröße von Report-Subtree v) - Zeit für alle Report-Subtree-Anfragen = O (Gesamt-Output-Größe) = O(|S ∩ [a, b]|). Satz 1.5 Obiger Algorithmus benötigt zum Aufbau der Datenstruktur (d.h. des Baumes T ) Zeit O(n log(n)) und Platz O(n); die Zeit pro 1D-Range-Query ist O(log(n) + k), wobei k die Outputgröße ist. 1.1.2 Der 2D-Fall Nun ist S = {p1 , . . . , pn } ⊆ R2 , pi = (xi , yi ). Es werden Bereiche der Form [a1 , b1 ] × [a2 , b2 ] angefragt. Wir bauen zuerst einen 1D-Suchbaum T1 für die Menge S1 = {x1 , . . . , xn } der ersten Koordinaten der Elemente von S auf. Wenn wir hierin 1D-Range-Query (T1 , [a1 , b1 ]) ausführen, erhalten wir als Ergebnis A1 = S ∩ ([a1 , b1 ] × R). Um die Anfrage für den Bereich [a1 , b1 ] × [a2 , b2 ] durchzuführen, könnten wir nun analog einen Baum T2 aufbauen, der S2 = {y1 , . . . , yn } verwaltet, um dort 1DRange-Query (T2 , [a2 , b2 ]) auszuführen. Wir würden dann A2 = S ∩ (R × [a2 , b2 ]) erhalten, und das gesuchte Ergebnis ist A1 ∩ A2 . Die Laufzeit ist dann O(log(n) + |A1 | + |A2 |). In vielen Fällen ist das jedoch sehr langsam, da |A1 | + |A2 | sehr groß sein kann, obwohl die wirkliche Outputgröße, |A1 ∩A2 |, sehr klein ist. Unser Ziel ist es, Laufzeit O(log(n)+k), k = Output-Größe, zu erreichen. Unser erster Algorithmus wird Zeit O(log(n)2 + k) erreichen. Dazu konstruieren wir zuerst den oben beschriebenen 1D-Suchbaum T1 für S1 = {x1 , . . . , xn }, die ersten Koordinaten der Punkte in S. Betrachte nun einen Knoten v von T1 . Sei Sv ⊆ S die Menge der Punkte aus S, deren erste Koordinate an den Blättern des Subbaumes mit Wurzel v gespeichert sind. Wir bauen nun einen 1D-Suchbaum Tv für die Menge der zweiten Koordinaten der Knoten in Sv auf. Die Gesamt-Struktur, T1 und die Tv ’s, bilden den 2D-Suchbaum T . Größe von T : Jedes pi ∈ S wird auf jedem Level von T in genau einem Tv abgespeichert, also insgesamt log(n) mal. Somit wird für T Platz O(n log(n)) benötigt. Zeit zum Aufbau von T : T1 benötigt Zeit O(n log(n)). Jedes Tv benötigt Zeit O(|Sv | log(|Sv |). Allerdings setzt sich diese Zeit zusammen aus: Sortieren von Sv (nach zweiter Koordinate (Zeit O(|Sv | log(|Sv |)) plus Zeit zum Aufbau von Tv aus sortiertem Sv (Zeit O(|Sv |). Wenn wir einmal zu Beginn ganz S nach zweiter Koordinate sortieren, können wir Tv in Zeit O(|Sv |) aufbauen. Da, wie oben gesagt, jedes pi ∈ S in log(n) vielen Mengen Sv liegt, ist Σ|Sv | = O(n log(n)), v d.h. wir können T in Zeit O(n log(n)) aufbauen. 2D-Range-Query (T, [a1 , b1 ] × [a2 , b2 ]) arbeitet wie 1D-Range-Query (T1 , [a1 , b1 ]), mit folgender Modifikation: Jeder Aufruf Report-Subtree (v) wird ersetzt durch 1D-Range-Query (Tv , [a2 , b2 ]). 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 11 Korrektheit: Analog wie für den 1D-Fall. Laufzeit: O(log(n)), um Pa1 , Pb1 und den Split-Knoten zu finden. Zusätzlich wird für l ≤ log(n) viele Knoten vi 1D-Range-Query (Tvi , [a2 , b2 ]) aufgerufen, Kosten O(log(|Svi | + kvi )), mit kiv = |Svi ∩ [a2 , b2 ]|. l Gesamtzeit= O(log(n) + Σ log(|Svi | + kvi ) i=1 l = O(log(n) + log(n)2 + Σ kvi ) i=1 = O(log(n)2 + k), mit k = |S ∩ ([a1 , b1 ] × [a2 , b2 ])| l (Beachte: Hier wird benutzt, daß k = Σ kvi gilt. Dazu überlege man sich, daß für i 6= j i=1 gilt: Svi ∩ Svj = ∅.) Satz 1.6 Obiger Algorithmus benötigt zum Aufbau der Datenstruktur Zeit und Platz O(n log(n)); die Zeit pro 2D-Range-Query beträgt O(log(n)2 + k), wobei k die OutputGröße ist. 1.1.3 Schnellere Antwortzeiten im 2D-Fall In diesem Abschnitt geben wir eine Methode an, die es erlaubt, die Zeit für eine Anfrage von O(log(n)2 + k) auf O(log(n) + k) zu reduzieren. Wir modifizieren dazu unsere Datenstruktur: Zuerst bauen wir wieder den Baum T1 auf, der ein 1D-Suchbaum bzgl. der ersten Koordinaten der Elemente aus S ist. Die Menge Sv ⊆ S, die zu einem Knoten v gehört, speichern wir allerdings nicht mehr als 1D-Suchbaum Tv (bzgl. der zweiten Koordinaten der Punkte in Sv ) ab, sondern in einem Array Av , sortiert bzgl. der zweiten Koordinaten. Wir werden nun zwischen Av und Av1 , Av2 (v1 , v2 sind die Kinder von v) Pointer installieren, die folgendes ermöglichen: Falls wir für ein a ∈ R in Av den kleinsten Eintrag p ≥ a kennen, können wir die entsprechenden Einträge in Av1 und Av2 in konstanter Zeit berechnen. Das ist einfach: Wir legen von jedem Eintrag p in Av je einen Pointer in Av1 und Av2 zu der Position des kleinsten Eintrags p0 ≥ p. (p, p0 sind die zweiten Koordinaten der Punkte). (vgl. Abbildung 2) Falls wir nun 2D-Range-Query ([a1 , b1 ] × [a2 , b2 ]) ausführen, berechnen wir wieder zuerst die Wege Pa1 , Pb1 und den Split-Knoten v̄ in T1 . In Av̄ berechnen wir die Position des kleinsten p0 ≥ a2 (p0 ist 2-te Koordinate des zugehörigen Punkts aus Sv ) durch binäre Suche, also in Zeit O(log(n)). Wir folgen nun bei dem weiteren Durchlauf des Weges Pa auch immer den Pointern von p0 aus auf diesem Weg. Für jeden Punkt v, für den wir im letzten Algorithmus 1D-Range-Query (Tv , [a2 , b2 ]) aufgerufen haben, können wir nun mit 14. Oktober 2008, Version 0.6 1.1 Orthogonal Range Searching 12 ... 3 4 5 6 7 8 ... Av ... 3 5 7 ... Av 2 ... 4 6 8 ... Av 1 Abbildung 2: Hilfe des Pointers direkt an die Position des kleinsten p00 ≥ a2 in Av springen, und von dieser Position aus nachfolgenden die kv vielen Punkte mit 2-ter Koordinate ≤ b2 in Zeit O(kv ) auslesen. Der log(n)-Aufwand taucht also nicht log(n), sondern nur einmal (bei der binären Suche in Av̄ ) auf! Also ergibt sich Suchzeit O(log(n) + k). Man kann sich einfach überlegen, daß auch diese Datenstruktur mit Zeit und Platz O(n log(n)) aufgebaut werden kann. Satz 1.7 Obiger Algorithmus benötigt zum Aufbau der Datenstrukturen Zeit und Platz O(n log(n)), die Zeit pro 2D-Range-Query ist O(log(n) + k), wobei k die Output-Größe ist. Die oben beschriebene Technik kann in allgemeiner Form für viele Datenstrukturprobleme eingesetzt werden. Man nennt sie Fractional Cascading. 1.1.4 Orthogonal Range Searching in hohen Dimensionen Die Algorithmen aus 1.2 und 1.3 können in natürlicher Weise auf höhere Dimensionen erweitert werden. In Rd ergibt sich Zeit und Platz O(n(log(n))d−1 ) für den Aufbau der Datenstruktur, Zeit O(log(n)d + k) (bzw. O(log(n)d−1 + k) für den Algorithmus aus 1.1.3) pro Anfrage. 1.1.5 Dynamisches Orthogonal Range Searching Wir betrachten nun die dynamische Variante unserer Range Searching Verfahren: Neben den Range-Queries sind nun auch Einfüge-und-Lösche-Operationen erlaubt, die einen Punkt zu S hinzufügen bzw. aus S löschen. Im 1D-Fall ersetzen wir den (statischen) Suchbaum T hierfür durch einen dynamischen Suchbaum wie z.B. den aus dem Grundstudium bekannten rot-schwarz-Baum. Damit können wir den gleichen Range-Query Algorithmus benutzen wie zuvor, haben aber zusätzlich die Eigenschaft, dass Einfügen und Löschen in Zeit O(log(n)) durchgeführt werden kann. 14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 13 Man kann mit recht aufwendigen Überlegungen die Version aus 1.1.3 dynamisieren. Dieses Verfahren stellen wir hier nicht genauer vor. Als Ergebnis halten wir jedoch fest: Satz 1.8 Dynamisches Orthogonal Range Searching in 2D kann mit Speicherbedarf O(n log n) durchgeführt werden, wobei Range-Queries Zeit O(log(n) + k) und Einfügen/Löschen Zeit O(log(n)) benötigen. Zum Abschluß bemerken wir noch folgendes: Wir können in den oben genannten Algorithmen auch Intervallgrenzen auf −∞ oder ∞ setzen, d.h. z.B. Range Query (T, [−∞, b1 ] × [−∞, b2 ]) durchführen. Einfache lineare Transformationen lassen es zudem zu, die Datenstrukturen so zu verallgemeinern, daß wir auch anstatt rechtwinkeliger, achsenparalleler Kegel [−∞, b1 ] × [−∞, b2 ] auch beliebige “Verschiebungen” eines festen, beliebigen Kegels abfragen können. Genauer: Betrachte einen festen Kegel K = {λ1 z1 + λ2 z2 , λ1 , λ2 ≥ 0} für feste z1 , z2 ∈ R2 . Für a ∈ R2 sei Ka := {x ∈ R2 |x − a ∈ K} 2 K 1 z1 z2 1 2 3 Abbildung 3: Wenn wir jetzt Range Queries für Ka für beliebiges a durchführen wollen (aber festen Kegel K!), können wir alle oben genannten Algorithmen benutzen, indem wir zu Beginn eine lineare Transformation auf S anwenden (welche?) und die Kegel Ka in rechtwinkelige, achsenparallele Kegel [−∞, b1 ] × [−∞, b2 ] transformieren. Eine Technik, die ausnutzt, daß wir es nur mit Kegel anstatt mit Rechtecken zu tun haben, erlaubt sogar die Reduktion des Speicherbedarfs auf O(n). Die dabei entstehende Datenstruktur nennt sich Priority Search Tree. 1.2 Circular Range Searching Anwendung: Computer Graphik, Walkthrough-Animation: Die Menge S = {p1 , . . . , pn } ⊆ R2 beschreibt z.B. Positionen von Häusern in der Ebene. Eine grundlegende Aufgabe be14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 14 steht darin, zu einer Besucherposition ∈ R2 und einem Radius t alle Häuser im Abstand höchstens t zu q zu berechnen. Eine Circular Range Query würde also mit Parametern q, t aufgerufen, und muß S ∩ K(q, t) berechnen, wobei K(q, t) den Kreis {x ∈ R2 , |q − x| ≤ t} bezeichnet. Unser Ziel ist es, hierfür Datenstrukturen zu entwerfen, die in Zeit O(n log(n)) auf Platz O(n) aufgebaut werden können, und Antwortzeiten O(log(n) + k) aufweisen, wobei k die Größe der Ausgabe ist. Das wird uns nicht vollständig gelingen, wir werden aber nahe herankommen. Wir werden uns auf den Fall beschränken, dass unsere Queries immer von der Form K(q, t) mit q ∈ S sind. 1.2.1 Spanner und weak Spanner Sei S = {p1 , . . . , pn } und f > 1. Ein (gerichteter oder ungerichteter) Graph G = (S, E) heißt f-Spanner, falls für jedes Knotenpaar pi , pj gilt: In G existiert ein (ggf. gerichteter) Weg W von pi nach pj , mit ||W || ≤ f · |pi − pj . Dabei bezeichnet ||W || die Länge von W , d.h. die Summe der (Euklidischen) Längen der Kanten auf W . f heißt Stretch-Faktor. G ist ein weak f -Spanner, falls für jedes Knotenpaar pi , pj gilt: In G gibt es einen (ggf. gerichteten) Weg von pi nach pj , der nie K(pi , t) verläßt, mit t = f · |pi − pj |. Beobachtung: Jeder f -Spanner ist auch weak f -Spanner. Satz 1.9 Sei G = (S, E) ein weak f -Spanner. Dann kann zu gegebenen p ∈ S, t > 0 die Menge K(p, t) ∩ S in Zeit O(k · D) berechnet werden. Dabei ist D der Grad bzw. Outgrad von G, und k = |K(p, f · t) ∩ S|. Beweis: Wir starten in p eine Breitensuche, brechen die Suche aber immer dann ab, wenn wir auf einen Knoten q mit |p − q| > f · t stoßen. Alle dabei gefundenen Knoten q, die zusätzlich |p − q| ≤ t erfüllen, bilden die Ausgabe. Korrektheit: Es werden nur Knoten q ∈ S mit |p−q| ≤ t ausgegeben. Andererseits: Sei q ∈ S, |p−q| ≤ t. Dann gibt es einen Weg von p nach q in G, der nie K(p, f · t) verläßt. Da die Breitensuche alle Knoten in K(p, f · t) besucht, findet sie auch diesen Weg und somit q. Also wird q ausgegeben. Laufzeit: Die Breitensuche benötigt konstante Zeit pro Besuch eines Knotens. Sie besucht alle Knoten in K(p, f · t) (k Knoten) sowie alle deren Nachbarn (höchstens D · k viele). 2 Wir werden im nächsten Kapitel gerichtete Spanner und weak Spanner mit konstantem Outgrad konstruieren. 14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 1.2.2 15 Sektorengraphen Sei wieder S = {p1 , . . . , pn } ⊆ R2 , k ∈ N, k gerade. Wir definieren einen gerichteten Graphen mit Grad k auf S wie folgt: Betrachte Knoten pi . Zerlege den Raum um pi in k Kegel, die Sektoren, definiert durch k/2 viele Geraden g1 , . . . , gk/2 durch pi . Dabei sind alle Winkel zwischen g1 und g2 , g2 und g3 , . . . , gk/2 und g1 gleich, nämlich 2π/k. In jedem Sektor V bestimmen wir einen Nachbarn von pi wie folgt: Wir definieren den Sektor-Abstand eines Punktes x ∈ V zu pi als den Euklidischen Abstand von pi zur Projektion x0 von x auf die Mittelsenkrechte von V. x x’ Sektor V 2 π p i Winkel = 6 Abbildung 4: Bsp.: K = 6 Wie verbinden nun pi in jedem Sektor V mit einem pj ∈ V mit minimalem Sektor-Abstand zu pi . Wir erhalten damit einen gerichteten Graphen Gk (S) mit Outgrad k, also höchstens n · k Kanten. Im folgenden nehmen wir an, daß S nicht degeneriert ist, d.h. dass kein Punkt aus S auf einer Sektorengrenze eines anderen Punktes aus S liegt. Satz 1.10 (i) Gk (S) kann in Zeit O(n log(n)) aus S konstruiert werden. (ii) Für k ≥ 6 ist Gk (S) ein weak Spanner mit Stretch-Faktor r γ p 2π min{ 1 + 48 sin4 ( ), 5 − 4 cos(γ)}, mitγ = . 2 k (iii) Für k ≥ 8 ist Gk (S) ein Spanner mit Stretch-Faktor 1 2π . γ , mitγ = 1 − 2 sin( 2 ) k 14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 16 Beispiel 1.11 Stretch-Faktoren für weak Spanner: k=6→f =2 k = 8 → f ≈ 1.47 k = 10 → f ≈ 1.33 Stretch-Faktoren für Spanner: k = 8 → f ≈ 4.26 k = 10 → f ≈ 2.62 Für k → ∞ gehen die Stretch-Faktoren für Spanner und weak Spanner gegen 1. Wir werden den Satz hier nicht vollständig beweisen. Wir zeigen nur (i), sowie (ii) für k = 6. Beweis des Satzes: zu (i) Wir benutzen die Sweep-Line Technik. Zur Berechnung der Nachbarn im l-ten Sektor Vj von pj gehen wir wie folgt vor: Zur einfacheren Veranschaulichung nehmen wir an, daß die Mittelsenkrechte von Vj für jeden Knoten parallel zur x-Achse verläuft, und Vj rechts von pj liegt. Wir sortieren S zuerst bzgl. der x-Koordinaten der pj . Nun benutzen wir zur Berechnung der Nachbarn eine Sweep-Line. Dieses ist eine Gerade M parallel zur y-Achse, die wir von links nach rechts über die x-Achse schieben, d.h., die wir hintereinander durch p1 , p2 , . . . , pn verschieben. Dabei sei immer D ⊆ S so, daß D folgende Invariante erfüllt. Falls die Sweep-Line bei pj angekommen ist, enthält D alle pj ∈ S, die links der Sweep-Line liegen und deren Nachbarn in Vj noch nicht gefunden wurden. Für i = 1 ist D = ∅. Sei D für Schritt i − 1, d.h. für die Situation, das die Sweep-line durch pi−1 läuft, berechnet. Wie entsteht das neue D für die Sweep-line durch pi ? (i) Sei U die Menge aller pj ∈ D, für die pj ∈ Vj gilt. Punkte pj ∈ U bekommen pi als Nachbarn in Vj , und werden aus D entfernt. (ii) pi wird zu D hinzugefügt. Man überlege sich, daß (i) korrekt ist, d.h. tatsächlich für alle pj ∈ U der Punkt pi mit geringsten Sektorenabstand in Vj hat. Damit ist dann auch die o.g. Invariante gewährleistet. Seien Di , Ui die Mengen D, U nach der i-ten Iteration. Wie berechnen wir Ui ? Wie aktualisieren wir Di−1 zu Dj ? Dazu überlegen wir uns folgendes. Vj0 entstehe aus Vj durch Spiegelung von Vj an der Senkrechten duch pj . Dann gilt: pj ∈ Vj genau dann wenn pj ∈ Vi0 . Somit ist Uj = Vj0 ∩ Di−1 , d.h. wir können Ui durch Range-Query (Vj0 ) bestimmen, die auf einer Datenstruktur für Di−1 arbeitet. Anschließend müssen wir jedes pj ∈ Ui aus Di−1 löschen, und dann pi einfügen. Das Ergebnis ist Di . Genau dieses kann ein Priority Search Tree (vgl. Ende Kapitel 1.1.5)! 14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 17 Laufzeit: Jedes pi wird nur einmal, nämlich in Dj eingefügt (wenn die Sweep-Line durch pj verläuft) und somit auch höchstens einmal aus einem Di gelöscht. Jede dieser höchstens 2n Operationen kostet Zeit O(log(n)). Die Laufzeit für die Range-Query mit Ergebnis Ui ist O(log(n) + |Ui |). Da aber pj aus D gelöscht wird, sobald es einmal in einem Ui auftaucht, kann kein pj in mehr als einem Ui auftauchen. Somit sind alle Ui disjunkt. Also ist die Gesamtlaufzeit aller Range-Queries n n Σ O(log(n) + |Ui |) = O(n log(n) + Σ |Ui |) = O(n log(n) + n) = O(n log n) i=1 i=1 Wenn wir diesen Algorithmus für alle k Sektoren durchführen, haben wir in Zeit O(n log(n)) den Sektorengraphen konstruiert. zu (ii) und (iii) Wir werden hier nicht (ii) und (iii) vollständig beweisen, sondern nur folgende zwei Dinge zeigen: Sei p, q ∈ S. Betrachte folgenden kanonischen p-q−Weg q1 , q2 , . . . in S : q1 = p. Für i > 1 ist qi der Nachbar von qi−1 in demjenigen Sektor von qi−1 in dem q liegt. Falls qi = q gilt, endet der Weg. Lemma 1.12 (i) Für k ≥ 6 ist der kanonische p-q−Weg endlich (und erreicht somit q). (ii) Für k = 6 liegt dieser Weg in K(p, 2|p − q|). Somit folgt aus dem Lemma, dass G6 (S) ein weak Spanner mit Stretch Faktor 2 ist. Wir werden die darüberhinaus gehenden Aussagen des Satzes hier nicht beweisen. Wir zeigen zuerst (ii). Sei |p − q| = t. Behauptung 1.13 Jedes gleichseitige Dreieck, welches eine Ecke x in K(q, t) hat, und auf der x gegenüberliegenden Seite q enthält, liegt vollständig in K(q, t). Beweis: Schulgeometrie. Wir zeigen nun, daß der kanonische p-q−Weg vollständig in K(q, t) liegt. (Da p auf dem Rand von K(q, t) liegt, ist also K(q, t) ⊆ K(p, 2t), d.h. der kanonische p-q−Weg liegt vollständig in K(p, 2t).) Wir zeigen obiges durch Induktion nach der Länge i des Weges. i = 1 : q1 = p liegt in K(q, t). i > 1 : qi−1 liegt nach Ind. Vor. in K(q, t). Sei V der Sektor von qi−1 , in dem q liegt, D das Dreieck, welches entsteht, wenn wir V entlang der durch q verlaufenden, auf der Mittelsenkrechten von V senkrecht stehenden Geraden abschneiden. D ist ein gleichseitiges Dreieck, welches nach obiger Betrachtung in K(q, t) liegt. Da aber qi in D liegen muß, ist somit auch qi ∈ K(q, t). Damit verläuft der gesamte kanonische p-q−Weg in K(q, t) und somit auch in K(p, 2t). 14. Oktober 2008, Version 0.6 1.2 Circular Range Searching 18 zu (i) von Lemma 1.12. Wir gehen, wie zu Beginn gesagt, von “nicht-degenerierten” Fällen aus. In unserer Situation heißt das ja, dass kein Punkt auf S auf den Sektorengrenzen eines anderen Punktes liegt. Man kann sich deshalb einfach überlegen, dass der (euklidische) Abstand |pi −q| der Punkte auf dem kanonischen p-q−Weg mit wachsendem i abnimmt, d.h.: |qi − q| < |qj − q| für alle i > j. Würde dieser Weg nie q erreichen, so würde, da S endlich ist, für irgendwelche i > j qi = qj gelten. Damit wäre natürlich auch |qi − q| = |qj − q|, ein Widerspruch. 2 Wir können also eine Datenstruktur in Zeit O(n log(n)) aufbauen, die es erlaubt, Range Queries für Kugeln K(q, t) in Zeit O(|S ∩ K(q, f · t)|) zu beantworten, falls q ∈ S ist. Dabei ist f der Stretch-Faktor des weak Spanners (z.B. f = 2 bei 6 Sektoren). Wenn wir solche Anfragen auch für K(q, t) mit q 6∈ S ermöglichen wollen, müssen wir erheblich mehr Aufwand treiben. Näheres hierzu findet sich in der Dissertation von Tamas Lukovszki. Weitere Informationen, auch über die Nutzung solcher Strukturen in unserem Walkthrough System PaRSIWal (Paderborn Realtime System for Interactive Walkthrough), finden sich im Netz unter http://www.unipaderborn.de/fachbereich/AG/agmadh/WWW/DFG-SPP/#pub. 14. Oktober 2008, Version 0.6 19 2 Realisierung von Wörterbüchern durch universelles und perfektes Hashing In diesem Kapitel werden wir kurz das Konzept der abstrakten Datentypen wiederholen, und insbesondere die Datentypen “Wörterbuch und statisches Wörterbuch” genauer anschauen. Im Grundstudium sind dynamische Suchbäume wie z.B. rot-schwarz Bäume untersucht worden. In dieser Vorlesung konzentrieren wir uns auf Hashing-Verfahren zur Realisierung von Wörterbüchern. 2.1 Abstrakte Datentypen und Datenstrukturen Ein abstrakter Datentyp (ADT) ist eine Menge von Objekten, zusammen mit auf den Objekten definierten Operationen. Abstrakte Datentypen muß man selbst implementieren, in der Programmiersprache vorgegebene primitive Datentypen (z.B. Integer, mit Operationen, +, -, *, DIV, MOD, ...) und strukturierte Datentypen (Array, File, Record, ...) können dazu benutzt werden. Die Implementation eines abstrakten Datentyps heißt Datenstruktur. Beispiel 2.1 Objekte: Mengen S von Elementen von gegebenem Grundtyp. Operationen: Create S erzeuge eine leere Menge S Insert (x, S) S := S ∪ {x} Lookup (x, S) suche nach x in S (Antwort: ja/nein) Delete (x, S) S := S \ {x} Min (S) gebe Minimum von S aus Max (S) gebe Maximum von S aus Deletemin (S) S := S \ {min(S)} Der ADT mit Operationen Insert, Deletemin, Min heißt Priority-Queue. Wir kennen eine Implementation: Heap −→ Der Heap ist eine Datenstruktur für Priority Queues. Zeit für Insert: Zeit für Min: Zeit für Deletemin: O(log(n)) O(1) O(log(n)) 14. Oktober 2008, Version 0.6 2.2 Einfache Datenstrukturen für Wörterbücher 20 Der ADT mit Operationen Insert, Delete, Lookup heißt Wörterbuch (Dictionary). Falls S vorgegeben ist, und in S nur Lookups durchgeführt werden, heißt der ADT statisches Wörterbuch (denn S wird nie verändert, bleibt also statisch). Beispiel 2.2 Folgen F von Elemente von gegebenem Grundtyp. Operationen: z.B.: Find (F, p): gebe p-tes Element von F aus Delete (F, p): ... Insert (F, p, x): Füge x in F an Position p ein Operationen, die Folgen verknüpfen: Concatenate (F1 , F2 ): Hänge F2 hinter F1 . Separate (F, p, F1 ): F = (a1 , . . . , an ) ⇒ F := (a1 , . . . , ap ), F1 := (ap+1 , . . . , an ) Copy (F1 , F2 ): F2 := F1 Dieser ADT wird häufig Liste genannt. Eine Datenstruktur für Listen ist die lineare Liste mit den entsprechenden Operationen. Bemerkung zu Wörterbüchern In Anwendungen sind die Objekte häufig Records (x, Info). Dabei ist x der Schlüssel, unter dem die Info abgespeichert ist. Bei Suchen (Lookups) wird dann der Schlüssel eingegeben, und als Antwort wird die zugehörige Info erwartet. Bei Insert wird Schlüssel x und Info eingegeben, und entweder neu eingefügt, oder, falls es schon (x, Info’) in S gibt, wird dieses durch (x, Info) überschrieben. Bei Delete wird nur ein Schlüssel eingegeben, der dann mitsamt seiner Info gestrichen wird. Beispiel 2.3 (N ame, Adresse, T elef onnummer) {z } | {z } | Schlüssel Info 2.2 Einfache Datenstrukturen für Wörterbücher statisch: a) S liegt in Array oder linearer Liste vor: Aufbauzeit: O(n) schlecht Suchzeit: O(n) b) S liegt im sortierten Array vor: Aufbauzeit: O(n log(n)) (Sortieren) Suchzeit: O(log(n)) (Binäre Suche) 14. Oktober 2008, Version 0.6 2.3 Suchbäume 21 dynamisch: a) wie a) oben (Lineare Liste) Einfügen/Streichen: O(n) b) Einfügen/Streichen: O(n) → Ziel: Kombiniere Vorteil des sortierten Arrays (schnelle Suchzeit) mit Möglichkeiten, es schnell zu verändern. 2.3 Suchbäume Im Grundstudium wurden Wörterbücher durch dynamische Suchbäume realisiert. Dadurch ergeben sich logarithmische Laufzeiten für Lookup, Insert und Delete, etwa bei Rot-Schwarz Bäume oder B-Bäumen. Wir werden in dieser Vorlesung Verfahren untersuchen, die bessere Laufzeiten liefern. Sie können allerdings nicht zusätzliche Operationen wie “Berechne zu x das nächstgrößere Objekt in S” unterstützen. 2.4 Hashing Einfache Idee für Wörterbuchimplementation, wenn S ⊆ U gilt, wobei U endlich ist: O.B.d.A. U = {0, . . . , p − 1}. Benutze Array A[0 : p − 1] mit A[i] ∈ {0, 1} und A[i] = 1 ↔ i ∈ S. Dann benötigen Insert, Delete, Lookup konstante Zeit! Aber: Sehr hoher Speicherplatzbedarf. Wir wollen den Speicherplatz reduzieren. Idee: Benutzte Funktion h : U → {0, . . . , m − 1}, eine Hashfunktion, und ein Array T [0 : m − 1], das Hashtableau. Speichere x ∈ S in T [h(x)] ab. Beispiel 2.4 m = 5, S = {3, 15, 22, 24} ⊆ {0, . . . , 99} = U, h(x) = x mod 5. T sieht wie 0 1 2 3 4 folgt aus T: 15 22 3 24 Problem: Es kann Kollisionen geben, d.h. Paare x, y ∈ S mit h(x) = h(y) (aber x 6= y). Im Beispiel würde etwa Einfügen von 27 (in T [2]) eine Kollision erzeugen, dort ständen dann 22 und 27. Die Güte von Hashverfahren hängt ab von - Wahrscheinlichkeit/Häufigkeit von Kollisionen. - Größe von m relativ zu n (Ideal: m = n oder m = cn, c kleine Kontstante) - Zeit um h auszuwerten. 14. Oktober 2008, Version 0.6 2.4 Hashing 2.4.1 22 Behandlung von Kollisionen: Hashing with Chaining Idee: Jedes T [i] ist lineare Liste, in der die Elemente {x ∈ S, h(x) = i} abgespeichert sind. Worst case Zeit für Einfügen, Löschen, Suchen: Θ(n). Best case Zeit bei m ≥ n: 0(1) (nämlich dann, wenn immer alle Listen Länge ≤ c, c konstant haben.) Durchschnittliche Laufzeit: Wir machen folgende Annahmen: a) Hashfunktionen sind uniform, d.h. für i ∈ {0, . . . , m − 1} gilt |h−1 (i)| ∈ {b mp c, b mp c}. (Auf jedes i ∈ {1, . . . , m} werden gleichviele Schlüssel abgebildet.) b) Die Elemente aus S sind unabhängig, zufällig mit Wahrscheinlichkeit Prob (x wird gewählt) = p1 ausgewählt. Dieses ist etwa von h(x) = x mod m erfüllt. Diese Hashfunktion kann außerdem in konstanter Zeit ausgewertet werden. Wir nehmen nun an, daß wir n Operationen ausgeführt haben, zur Zeit eine Menge S von l Elementen, l ≤ n, abgespeichert ist, und dann Insert (x), Delete (x) oder Lookup (x) für ein zufälliges x ∈ U ausgeführt wird. Dann ist für jedes i ∈ {0, . . . , m − 1} die Wahrscheinlichkeit für (h(x) = i) = 1 . m m Sei nun Bi = h−1 (i) ∩ S das i-te Bucket bi = |Bi | seine Größe. Wir wissen: Σ bi = l ≤ n. i=1 n . Also hat bi durchschnittliche Größe ml ≤ m Falls h(x) = i ist, benötigt die Operation Insert (x), Delete (x), Lookup (x) höchstens bi + 1 Schritte. Also: Durchschnittliche Zahl von Schritten für diese Operation m ≤ Σ Prob (h(x) = i) · (bi + 1) = i=1 1 m m · Σ (bi + 1) = i=1 1 (m m + l) ≤ 1 + n m Satz 2.5 Sei m ≥ n. Um in einem Wörterbuch, daß durch Hashing mit Chaining implementiert wird, auf einer Menge S, |S| ≤ n, eine Operation Insert (x), Delete (x) oder Lookup (x) durchzuführen, wird im worst case Zeit Θ(n), im best case Zeit O(1), im Durchn schnitt Zeit O(1 + m ) = O(1) benötigt, und Speicherplatz O(m). 2.4.2 Universelles Hashing und Worst Case Expected Time Untersuchungen Das Problem der durchschnittlichen Laufzeit: Inputs sind im allgemeinen nicht zufällig. Beim Quicksort haben Sie im Grundstudium Probabilistische Algorithmen kennengelernt, in denen der Ablauf der Rechnung nicht nur vom Input, sondern auch von Zufallsexperimenten abhängt. Es wurden nur Algorithmen betrachtet, die immer, also unabhängig von den Ergebnissen der Zufallsexperimente, korrekt sind. Die Laufzeit bei festem Input x 14. Oktober 2008, Version 0.6 2.4 Hashing 23 hängt allerdings von diesen Experimenten ab, wir messen sie deshalb als erwartete Zeit (≈ durchschnittliche Zeit, Durchschnitt gebildet über alle Laufzeiten bei Eingabe x, die sich durch die verschiedenen Ergebnisse der Zufallsexperimente ergeben.) Der worst case über die erwarteten Zeiten, worst case genommen über alle Inputs, der Länge n, ist dann T (n), die worst case expected time. Im folgenden wird das Zufallsexperiment des Algorithmus darin bestehen, eine zufällige Funktion h aus einer Menge H von Hashfunktionen auszuwählen. Definition 2.6 Eine Menge H ⊆ {h|h : U → {0, . . . , m − 1}} ist c-universell, falls für alle x, y ∈ U, x 6= y, gilt: #{h|h ∈ H und h(x) = h(y)} ≤ c · |H|/m. Satz 2.7 p = #U sei Primzahl. Dann ist H1 (m) = {ha,b |a, b ∈ U ha,b (x) = ((ax + b) mod p mod m) c-universell, mit c = (b mp c/ mp )2 (≈ 1). Beweis: - #H1 (m) = p2 - Sei x, y ∈ U, x 6= y. zz: #{(a, b) ∈ U |ha,b (x) = ha,b (y)} ≤ cp2 /m (Dann folgt der Satz) Beweis: ha,b (x) = ha,b (y) ⇔ ∃q ∈ {0, . . . , m − 1} und r, s ∈ {0, . . . , bp/mc − 1} mit ax + b = q + rm mod p ay + b + q + sm mod p Für festes r, s, q gibt es genau eine Lösung a, b für obiges Gleichungssystem , da p Primzahl ist, das Gleichungssystem also im Körper Zp gelöst wird. ⇒ #{(a, b)|ha,b (x) = ha,b (y)} = #{(q, r, s)|q ∈ {0, . . . , m − 1}, r, s ∈ {0, . . . , bp/mc − 1} = p p 2 m · b mp c2 = (b c/ )2 pm 2 | m {z m } =c Folgende Datenstruktur betrachte für das Wörterbuchproblem. - Wähle zufällig ein h ∈ H, wobei H c-universell für eine Konstante c ist, und jedes h ∈ H in konstanter Zeit erzeugt (i.e. aus konstant vielen Zufallszahlen in konstanter Zeit berechnet) werden kann, und in konstanter Zeit ausgewertet werden kann. (Die obige Klasse H1 (m) ist z.B. ok). - Führe mit diesem h Hashing with Chaining durch. Satz 2.8 Sei m ≥ n. Um in einem Wörterbuch, dass nach obigem Algorithmus implementiert wird, auf einer Menge S, #S ≤ n, beliebige Operationen Insert (x), Delete (x), Lookup (x) durchzuführen, wird worst case expected time O(1 + c · n/m) = 0(1) benötigt, und Speicherplatz 0(m). 14. Oktober 2008, Version 0.6 2.4 Hashing 24 Beweis: ( 1 h(x) = h(y), x 6= y Sei δh (x, y) = 0 sonst , δh (x, S) = Σ δh (x, y)(=# ˆ Elemente in S, die mit x kollidieren) y∈S Es gilt: Erwartete Kosten für Operation (x) = 1 + erwartete # Elemente in S, mit denen x kollidiert 1 1 Σ (1 + δh (x, S)) = (#H + Σ Σ δh (x, y)) #H = #H h∈H h∈H = (#H + Σ ( Σ δh (x, y))) · ≤ (#H + y∈S h∈H 1 Σ (c · #H )) #H m y∈S y∈S 1 #H n 1 n ≤ (#H · (1 + c · m )) · #H = 1 + cm - Wir erhalten also auch konstante worst case expected 2 time pro Operation. 2.4.3 Perfektes Hashing und worst case konstante Zeit für Lookups Der Idealfall beim Hashing sieht so aus, dass die Hashfunktion auf S injektiv ist, denn dann haben alle Listen Länge ≤ 1, und der Zugriff benötigt worst case konstante Zeit. Definition 2.9 Sei S ⊆ U, #S = n, m ≥ n. h : U → {0, . . . , m − 1} heißt perfekte Hashfunktion für S falls h|S injektiv ist. Wir suchen Methoden, um zu gegebener Menge S effizient eine perfekte Hashfunktion für S zu konstruieren, die wir in konstanter Zeit auswerten können, und die nur O(n) Platz braucht. Satz 2.10 Für H1 (m) (vgl. letztes Kapitel) gilt: Sei S ⊆ U beliebig, #S = n. Dann: 4n2 i=0 m gilt für mindestens die Hälfte der h ∈ H1 (m). Dabei ist m−1 Σ (bhi )2 < n + Bih := h−1 (i) ∩ S, m−1 Beweis: Σ ( Σ (bhi )2 − n) h∈H1 i=0 =n m−1 z }| { m−1 = Σ ( Σ (bhi )2 − Σ bhi ) h∈H1 i=0 i=0 14. Oktober 2008, Version 0.6 bhi = #Bih 2.4 Hashing 25 m−1 = Σ Σ (# geordneter P aare aus Bih ) h∈H1 i=0 m−1 = Σ Σ (#{(x, y) ∈ S 2 , h(x) = h(y) = i, x 6= y}) h∈H1 i=0 = Σ #{(x, y) ∈ S 2 , h(x) = h(y), x 6= y} h∈H1 = Σ #{h|h ∈ H1 , h(x) = h(y)} Σ2 c· (x,y)∈S 2 ,x6=y ≤ (x,y)∈S ,x6=y c·n2 ·p2 m < p2 · p2 m (siehe Beweis zu Satz 2.7) 2n2 m Annahme: mehr als die Hälfte der h ∈ H1 erfüllt: Σ(bhi )2 − n ≥ 4n2 /m. 2 2 ⇒ Σ (Σ(Bih )2 − n) ≥ p2 · 4n2 /m = p2 · 2n m h∈H1 =⇒ Widerspruch zu obiger Rechnung. Also: Für mindestens die Hälfte der h ∈ H1 gilt: Σ(bhi ) < 4n2 m + n. 2 Korollar 2.11 Sei S ⊆ U gegeben, #S ≤ n. Ein probabilistischer Algorithmus kann in erwarteter Zeit O(n) ein a) h ∈ H1 (n) mit Σ(bhi )2 ≤ 5n, bzw. b) h ∈ H1 (2n2 ), das perfekt für S ist, finden. Beweis: a) Wähle zufälliges h ∈ H1 (n), teste ob Σ(bhi )2 ≤ 5n ist (Zeit O(n)). Falls nicht, versuche noch einmal. Wegen Satz 2.10 mit m = n folgt, dass im Durchschnitt 2 Versuche reichen. b) Bei m = 2n2 hat man im Durchschnitt nach 2 Versuchen ein h gefunden mit Σ(bhi )2 < n + 2 (vgl. Satz 2.10 mit m = 2n2 ). Ein solches h ist jedoch immer injektiv auf S, denn wäre ein bi ≥ 2, so wäre Σ(bhi )2 ≥ 22 + n − 2 = n + 2. 2 Wir können nun eine probabilistische Konstruktion für ein perfektes Hashschema für S angeben. 1. Konstruiere probabilistisch ein h ∈ H1 (n) mit Σ(bhi )2 ≤ 5n. Benutze ein Array i−1 T1 [0 : n − 1] und schreibe nach T1 [i] die Werte 2(bhi )2 und di = Σ 2(bhj )2 , d0 = 0. j=0 2. Für i = 0, . . . , n − 1 : Konstruiere probabilistisch hi = ((ax + b) mod p) mod (2(bhj )2 ) ∈ H1 (2(bhj )2 ), das perfekt für Bjh ist. Schreibe auch a und b nach T1 [i]. 14. Oktober 2008, Version 0.6 2.4 Hashing 26 3. Lege Array T2 [0 : 10n − 1] an, schreibe x ∈ S nach hh(x) (x) + dh(x) . Beachte: 1. Da Σ2(bhj )2 ≤ 10n ist, ist T2 genügend groß. 2. Nach Konstruktion gibt es in T2 keine Kollisionen. 3. Um für x ∈ U den Wert hh(x) (x) + dh(x) auszurechnen, genügt konstante Zeit, falls h, hj , dj für j = h(x), bekannt sind. h ist sowieso bekannt, dj steht in T1 [j], ebenso eine Beschreibung von hj (durch a, b, 2(bhj )2 ). 4. Also kann obiges perfektes Hashschema für S in erwarteter Zeit O(n) aufgebaut werden. Lookup (x): x ∈ S ⇔ T2 [hh(x) (x) + dh(x) ] = x. Nach obigem kann dieser Test in konstanter Zeit durchgeführt werden. Satz 2.12 Obiges perfektes Hashschema für S liefert ein statisches Wörterbuch für S, dass erwartete (worst case exp. time) Aufbauzeit O(n), Platzbedarf O(n) hat, und worst case Zeit O(1) für Lookups garantiert. Bemerkung 2.13 Man kann mit Hilfe des obigen Schemas sogar ein dynamisches Wörterbuch implementieren, mit worst case expected time O(1) für Einfügen/Löschen, aber sogar worst case O(1) Zeit für Lookups. Sehr aufwendige Techniken liefern sogar, dass bei linearem Speicherbedarf jede Operation - Einfügen, Löschen, Lookup - in konstanter Zeit möglich ist, mit Wahrscheinlichkeit 1 − n1l . Dabei ist l eine beliebige Konstante, n die aktuelle Größe des Wörterbuchs. 14. Oktober 2008, Version 0.6 27 3 Flüsse in Netzwerken Ein Transportnetzwerk N (kurz: Netzwerk) ist gegeben durch • einen zusammenhängenden gerichteten Graphen G = (V, E) • eine Kapazitätsfunktion c : E → R+ • Quelle s ∈ V und Senke t ∈ V . N kann etwa ein Telefon- oder Rechnernetz beschreiben, die Kapazitäten beschreiben die Bandbreiten der einzelnen Verbindungen. Wir stellen uns nun die Aufgabe, soviel wie möglich Bandbreite für die Kommunikation von s nach t zu realisieren. Ein Fluss in N ist eine Funktion f : E → R mit folgenden Eigenschaften. (1) Kapazitätsrestriktion: ∀e ∈ E : 0 ≤ f (e) ≤ c(e). (2) Erhaltungsgesetz (Kirchhoff-Regel): ∀v ∈ V, v 6= s, t : Σ f (e) = e∈in(v) Σ f (e). e∈out(v) Dabei bezeichnet in(v) die Menge der in v hinein gerichteter Kanten, out(v) die Menge der aus v hinaus gerichteten Kanten. Der Wert von f ist val(f ) := Σ f (e) − e∈out(s) Σ f (e). e∈in(s) Typischerweise ist in(s) = ∅ (und out(t) = ∅), dann ist der Wert des Flusses gerade der Gesamtfluss, der s verlässt. Ziel dieses Kapitels ist es, Algorithmen zu entwickeln, die zu gegebenem Netzwerk einen maximalen Fluss, d.h. einen Fluss mit maximalem Wert berechnen. Den Wert eines solchen Flusses bezeichnen wir mit fmax . Dazu werden wir zuerst ein “Optimalitätskriterium” für Flüsse nachweisen, das uns gleichzeitig eine Basismethode zur Berechnung maximaler Flüsse liefert. Einige Eigenschaften von Flüssen: • Ist W ein gerichteter Weg von s nach t in N und 0 < r ≤ min{c(e), e ∈ W }, so ist f : E → R+ mit [f (e) = r für e ∈ W, f (e) = 0 sonst] ein Fluss in N . • Seien f, f 0 Flüsse in N , f + f 0 ≤ c. Dann ist f + f 0 ein Fluss in N . 14. Oktober 2008, Version 0.6 3.1 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) 3.1 28 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) Ein Schnitt in N ist eine disjunkte Zerlegung von V in S und T , mit s ∈ S, t ∈ T . Die Kapazität von (S, T ) ist die Summe der Kapazitäten der Kanten, die von S nach T laufen, c(S, T ) := Σ c(e) e∈E∩(S×T ) Mit cmin bezeichnen wir den Wert eines minimalen Schnittes, d.h. cmin := min{c(S, T ), (S, T ) Schnitt in N } Satz 3.1 (Satz von Ford und Fulkerson, Max-Flow Min-Cut Theorem) Für jedes Netzwerk gilt fmax = cmin , d.h., der Wert eines maximalen Flusses ist gleich dem Wert eines minimalen Schnittes. Beweis: (a) zz: fmax ≤ cmin Wir zeigen dazu: Sei f ein Fluss, (S, T ) ein Schnitt in N . Dann gilt val(f ) ≤ c(S, T ). Bew: f (S, T ) := f (e) − Σ e∈E∩(S×T ) Σ e∈E∩(T ×S) f (e) bezeichne den Flusswert des Schnittes (S, T ). Es gilt: (i) f (S, T ) ≤ (ii) Σ f (e) ≤ c(S, T ) e∈E∩(S×T ) (i) gilt, da f (e) ≥ 0 also Σ e∈E∩(T ×S) f (e) ≥ 0 ist. (ii) gilt, da f (e) ≤ c(e) ist. Wie groß ist f (S, T )? Aus dem Erhaltungsgesetz folgt direkt, das alle f (S, T ) gleich sind, also auch z.B. gleich f ({s}, V \ {s}) = val(f ). (b) zz: fmax ≥ cmin Für diesen Beweis beschreiben wir im folgenden einen Algorithmus, der einen Fluss mit Wert cmin berechnet. 2 14. Oktober 2008, Version 0.6 3.1 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) 29 Der Basisalgorithmus von Ford und Fulkerson Erste (falsche!) Idee: Erhöhe einen bereits gefundenen Fluss f (zu Beginn: f ≡ 0) durch folgende Prozedur: Suche in N einen gerichteten Weg W von s nach t, auf dem keine Kante e saturiert ist, d.h. für keine Kante e gilt f (e) = c(e). Erhöhe den Fluss auf allen Kanten von w um min{c(e) − f (e), e Kante auf W }. Falls kein solcher Weg existiert, gebe f aus. Folgendes Beispiel zeigt, dass dieser sehr intuitive Greedy-Algorithmus nicht optimal ist. a 1 b 1 1 1 s t 1 1 c 1 d Abbildung 5: Falls wir zuerst den Weg s − a − d − t wählen, erhalten wir einen Fluss mit Wert 1, den obiger Algorithmus nicht vergrößern kann. Der maximale Fluss hat aber offensichtlich den Wert 2. Eine gute Idee: Betrachte einen ungerichteten Weg von s nach t. Kanten auf W , die in Richtung von s nach t gerichtet sind, heißen Vorwärtskanten, die anderen Rückwärtskanten. Sei f ein Fluss in N . Die Restkapazität r(e) (bzgl. f ) einer Vorwärtskante e auf W ist c(e) − f (e), die einer Rückwärtskante f (e). Falls r = min{r(e), e Kante auf W } > 0 ist, heißt W vergrößernder Weg für (N, f ), r ist seine freie Kapazität. Lemma 3.2 W sei ein vergrößernder Weg für Fluss f in N mit freier Kapazität r. Falls wir f auf W um r vergrößern, d.h. falls wir für jede Vorwärtskante e von W f (e) um r vergrößern, für jede Rückwärtskante f (e) um r verringern, erhalten wir einen Fluss f 0 mit val(f 0 ) = val(f ) + r. Beweis: Einfache Übungen. 14. Oktober 2008, Version 0.6 2 3.1 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) 30 Der Basisalgorithmus von Ford und Fulkerson Eingabe: Netzwerk N . Starte mit dem leeren Fluss f ≡ 0. While Es gibt vergrößernden Weg W für f in N Do vergrößere f auf W um die freie Kapazität r von W Od Return f . Folgendes Lemma schließt den Beweis des Max-Flow Min-Cut Theorems ab und zeigt die Optimalität des Basisalgorithmus, d.h. zeigt, dass der Basisalgorithmus einen maximalen Fluss findet. Lemma 3.3 Es gibt einen Schnitt (S, T ) in N , so dass für den vom Basisalgorithmus berechneten Fluss f gilt: val(f ) = c(S, T ). Beweis: Wir stellen uns vor, wir suchen in N einen vergrößernden Weg durch Breitensuche von s aus in der ungerichteten Version von N . Die Breitensuche hat zu Beginn die Knotenmenge S = {s} besucht. Falls Knotenmenge S besucht ist, und ein Nachbar w eines v ∈ S untersucht wird, wird w zu s hinzugefügt, falls gilt: Entweder (v, w) ∈ E ((v, w) ist Kandidat für eine Vorwärtskante) und c(v, w) − f (v, w) > 0 oder (w, v) ∈ E ((v, w) ist Kandidat für eine Rückwärtskante) und f (w, v) > 0. Falls dieser Algorithmus t erreicht, ist offensichtlich auch ein vergrößernder Weg gefunden. Falls er nicht t erreicht, wird er eine Menge S ⊆ V erreicht haben, mit s ∈ S, t 6∈ S. Sei T = V \ S. Die Arbeitsweise der oben skizzierten Breitensuche impliziert: • f (v, w) = c(v, w) für alle (v, w) ∈ E ∩ (S × T ) • f (v, w) = 0 für alle (v, w) ∈ E ∩ (T × S) Somit gilt val(f ) = f (S, T ) = f (v, w) − Σ (v,w)∈E∩(S×T ) Σ | = Σ f (v, w) {z } (v,w)∈E∩(T ×S) = 0 c(v, w) = c(S, T ). (v,w)∈E∩(S×T ) 2 14. Oktober 2008, Version 0.6 3.1 Das “Max-Flow Min-Cut Theorem’ (Satz von Ford und Fulkerson) 31 Zur worst-case Laufzeit des Basisalgorithmus: • Zeit pro Flussvergrößerung (Breitensuche): O(|E|). • # Flussvergrößerungen: Θ(val(f )) im schlimmsten Fall, auch wenn alle Kapazitäten ganzzahlig sind. Begründung: val(f ) reicht bei ganzzahligen Kapazitäten immer aus, da die freie Kapazität eines vergrößernden Weges dann immer eine positive natürliche Zahl, also ≥ 1 ist. Folgendes Beispiel zeigt, dass val(f ) Schritte nötig sein können (falls man ungeschickte vergrößernde Wege wählt). Der Basisalgorithmus könnte als vergrößernde Wege immer abwechselnd s − a − b − t und s − b − a − t wählen, jeweils erhält man nur freie Kapazität 1, benötigt also 2 C Flussvergrößerungen. a C C 1 s t C C b Abbildung 6: Da wir in der Eingabe die Kapazitäten binär kodieren, ist die Eingabegröße für obiges Netzwerk O(log(C)). Somit hat der Basisalgorithmus exponentielle Laufzeit! Einige Bemerkungen: • Falls die Kapazitäten ganzzahlig sind, gibt es auch einen ganzzahligen maximalen Fluss. • Bei ganzzahligen Kapazitäten können auch (zusätzlich zu den ganzzahligen) nicht ganzzahligen maximale Flüsse existieren (Beispiel?). • Jeder ganzzahlige Fluss kann als Summe von ganzzahligen, gerichteten Wegflüssen beschrieben werden. (Gerichteter Weg-Fluss: Fluss, der auf einem gerichteten Weg von s nach t einen ganzzahligen Wert r hat, sonst überall 0.) (Dieses entspricht ja der Intuition von Transportnetzen!) 14. Oktober 2008, Version 0.6 3.2 Effiziente Algorithmen zur Berechnung maximaler Flüsse 32 • Man kann das Flussproblem als ein lineares Programm beschreiben. Die Variablen dieses Programms sind {f (e), e ∈ E}. Die (linearen) Restriktionen (hier: Gleichungen und Ungleichungen) sind die Kapazitätsrestriktionen (eine pro Kante) und Erhaltungsgesetze (eines pro Knoten). Die zu maximierende Zielfunktion ist val(f )(= Σ f (e) − Σ f (e)) e∈out(s) e∈in(s) • Das Max-Flow Min-Cut Theorem ist in diesem Sinne ein Spezialfall des Dualitätssatzes der linearen Optimierung. • Diese spezielle Klasse von linearen Programmen haben eine sehr schöne, für lineare Programme ungewöhnliche Eigenschaft: Falls die Koeffizienten des Programms (d.h. hier: die Kapazitäten) ganzzahlig sind, gibt es auch eine ganzzahlige optimale Lösung, nämlich den (wie oben gezeigt ganzzahligen) maximalen Fluss.) 3.2 Effiziente Algorithmen zur Berechnung maximaler Flüsse Wir haben uns im letzten Kapitel klar gemacht, dass der Algorithmus von Ford und Fulkerson zwar korrekt arbeitet, aber im schlimmsten Fall auch bei ganzzahligen Kapazitäten pro Flussvergrößerung den Wert des Flusses nur um 1 erhöht, und so exponentielle (in der binären Eingabenlänge) Laufzeit haben kann. Folgende einfache Modifikation (von Edmonds und Karp) liefert eine weit bessere Laufzeit: Wähle zur Flussvergrößerung jeweils einen kürzesten vergrößernden Weg. Ein solcher Weg kann mit Breitensuche ebenfalls in Zeit O(|E|) gefunden werden. Satz 3.4 Der obige Algorithmus von Edmonds und Karp benötigt höchstens |E| · |V |/2 viele Flussvergrößerungen, also Zeit O(|E|2 · |V |). Wir werden nicht diesen Satz, sondern ein stärkeres Resultat beweisen. Dabei ist die Grundidee, pro Runde nicht nur entlang eines kürzesten vergrößernden Weges den Fluss zu erhöhen, sondern alle kürzesten vergrößernden Wege auf einmal zu betrachten. Dazu berechnen wir zuerst zu einem Netzwerk N = (V, E) und einen Fluss f das Schichtennetzwerk LN für f . (LN : Levelled Network) Sei E1 = {(v, w), (v, w) ∈ E, f (v, w) < c(v, w)} E2 = {(w, v), (v, w) ∈ E, f (v, w) > 0} (Die Kanten von E1 (E2 ) können auf einem vergrößernden Weg als Vorwärts- (Rückwärts-) Kanten genutzt werden.) Wir definieren die Knotenwege V̄ von LN als V̄ = {v ∈ V, v ist durch Kanten aus E1 ∪ E2 von s aus erreichbar}. Für i ≥ 0 ist V̄i = {v ∈ V , der kürzeste Weg von s nach v über Kanten aus E1 ∪ E2 hat Länge i} die i-te Schicht. 14. Oktober 2008, Version 0.6 3.2 Effiziente Algorithmen zur Berechnung maximaler Flüsse Wir definieren die Kantenmenge Ē von LN als Ē = (E1 ∪ E2 ) ∩ Kapazitätsfunktion c̄ : Ē → R+ als c̄(e) = c(e) − f (e), falls e ∈ E1 , und c̄(e) = f (e), falls e ∈ E2 . 33 S i≥0 (Vi × Vi+1 ), und die Das folgende Lemma fasst einige einfache Eigenschaften von LN zusammen. Lemma 3.5 Sei LN das Schichtennetzwerk zu N, f . (i) LN kann in Zeit O(|E|) aus N, f berechnet werden. (ii) f¯ sei ein Fluss in LN . Dann ist f 0 : E → R+ mit f 0 (v, w) = f (v, w) + f¯(v, w) − f¯(w, v) ein Fluss in N und val(f 0 ) = val(f ) + val(f¯). (Falls (v, w) oder (w, v) ∈ / Ē ist, definieren wir f¯(v, w) = 0 bzw. f¯(w, v) = 0.) (iii) f ist maximaler Fluss in N , genau dann wenn t ∈ / V̄ . Beweis: zu (i): Breitensuche, einfache Übung. zu (ii): Einfaches nachrechnen liefert, dass Kapazitätsrestriktionen und Erhaltungsgesetze für f 0 gelten. Anschaulich: Jeder kürzeste vergrößernde Weg in N mit Restkapazität r findet sich als gerichteter Weg in LN wieder: Seine Vorwärtskanten sind in E1 , seine Rückwärtskanten sind in E2 , seine Restkapazität ist ebenfalls r. zu (iii): “⇐” Falls t ∈ / V̄ ist, gibt es in LN keinen vergrößernden Weg, also nach obigem auch keinen vergrößernden Weg in N . Somit ist f maximal. “⇒” Falls t ∈ V ist, gibt es in LN einen vergrößernden Weg (jeder Weg von s nach t in LN ist vergrößernd!). Der zugehörige Fluss in LN kann nach (ii) zur Vergrößerung von f genutzt werden. 2 Wir können nun ein Schema zur Berechnung maximaler Flüsse vorstellen. Dazu benötigen wir das Konzept eines Sperrflusses in LN . Sperrfluss-Algorithmus Begin (1) f :≡ 0 (Wir starten mit dem leeren Fluss in N .) (2) LN := Schichtennetzwerk für (N, f ) (3) While t ∈ V̄ (4) Do berechne Sperrfluss f¯ in LN (5) f := f vergrößert gemäß f¯ (vgl. Lemma 3.5 (ii)) (6) LN := Schichtennetzwerk für (N, f ) Od 14. Oktober 2008, Version 0.6 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus 34 End Schritt (2) bzw. (6) benötigt Zeit O(|E|) (Breitensuche). Nach Lemma 3.5 berechnet dieser Algorithmus einen maximalen Fluss in N . Fragen: (1) Wieviele Iterationen der While-Schleife sind notwendig? (2) Wie teuer ist Zeile 4 (Berechnung eines Sperrflusses?) Zu Frage 1: Betrachte ein Schichtennetzwerk LN mit l Schichten und darin einen Sperrfluss f¯. Sei f 0 der Fluss in N , der sich gemäß Lemma 3.5 (ii) aus f und f¯ ergibt, LN 0 das Schichtennetzwerk für (N, f 0 ). LN 0 habe l0 Schichten. Behauptung 3.6 l0 > l. Beweis: Die Kantenmenge Ē 0 von LN 0 kann sich von Ē, der Kantenmenge von LN , nur bei von f¯ saturierten Kanten (v, w) unterscheiden, und zwar wie folgt: (v, w) ist nicht in Ē 0 , allerdings ist (w, v) ∈ Ē 0 , auch wenn (w, v) ∈ / Ē ist. 0 Betrachte nun einen gerichteten Weg W der Länge l von s nach t in LN 0 . W enthält für mindestens eine saturierte Kante (v, w) ∈ Ē die Kante (w, v), sonst wäre W ja auch ein gerichteter Weg von LN gewesen. Das ist jedoch nicht möglich, da alle solche Wege eine saturierte Kante enthalten, die aber in LN 0 fehlt. Man mache sich klar: Falls W für s viele saturierte Kanten (v, w) ∈ Ē die Kante (w, v) enthält, hat W die Länge l + s . Da nach obigem s ≥ 1 gilt, folgt l0 > l. 2 Somit wird in jeder Iteration der While-Schleife die Schichtenzahl von LN um mindestens eins vergrößert. Da diese Zahl zu Beginn ≥ 2, am Ende ≤ |V | ist, folgt: Lemma 3.7 Der Sperrfluss-Algorithmus benötigt höchstens |V | − 2 Iteration der WhileSchleife, also Zeit O(|V | · (|E|+ “Zeit zur Berechnung eines Sperrflusses”)). 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus Das “Maximum-Flow”-Problem ist nun auf die (n − 2)-fache Berechnung von Sperrflüssen in LN reduziert. Wir können daher N vergessen und zur einfachen Notation LN := (V, E, c, s, t) übergehen. Mit einer Exploration von LN können wir durch Elimination nutzloser Knoten in Zeit O(|E|) erreichen, dass für alle v ∈ V : ∗ (∗) s −→ v LN ∗ −→ t LN d.h., v ist von s aus und t von v aus erreichbar. Die Berechnung eines Sperrflusses in LN basiert auf dem Konzept des Potentials eines Knotens v, gegeben durch 14. Oktober 2008, Version 0.6 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus Σ c(e) − f (e), P O[v] := min e∈in(v) bzw. P O[s] = c(e) − f (e) Σ e∈out(v) Σ c(e) − f (e), P O[t] = e∈out(s) 35 Σ c(e) − f (e). e∈in(t) P O[v] gibt die maximale Flussvergrößerung durch Knoten v an. P O∗ := min{P O[v]|v ∈ V } ist das minimale Potential der Knoten. Sei l die Schicht, in der P O∗ angenommen wird und v ∈ Vl mit P O[v] = P O∗ der betreffende Knoten. Idee: Erhöhe mit den Prozeduren Forward und Backward den Fluss in LN um P O∗ Einheiten. Forward propagiert P O∗ Einheiten von v aus schichtenweise nach vorne bis zur letzten Schicht Vk = {t}. Analog propagiert Backward P O∗ Einheiten von v aus schichtenweise nach hinten zur Schicht V0 = {s}. Da P O∗ minimal gewählt war, erhält kein Knoten mehr Einheiten als er propagieren kann. Nach erfolgter Propagation kann LN so vereinfacht werden, dass nach Löschen nutzloser Kanten und Knoten i) alle saturierten Kanten aus LN entfernt sind, ii) alle überlebenden Knoten wieder (∗) erfüllen, iii) alle Kanten mit einem entfernten Knoten ebenfalls entfernt sind. Beachte: Zumindest v mit allen inzidenten Kanten wird entfernt. Daher muß sich nach spätestens n Iterationen der geschilderten Idee ein Sperrfluss ergeben. Wir geben nun die Umsetzung dieser Idee an. Datenstrukturen: layer [x]: Index l mit x ∈ Vl (Schicht, die x enthält) S[x]: Überfluss bei Knoten x (der noch vorwärts bzw. rückwärts propagiert werden muß). Sh : die Knoten mit Überfluss in Schicht h, doppelt repräsentiert mit einer Liste und einem Bitvektor. (Mit dem Bitvektor kann MEMBER in O(1) Schritten getestet und somit Duplikate in der Liste vermieden werden). DEL : Liste nutzloser Knoten. P O[x] : Potential des Knotens x. out(x) : Liste der von x ausgehenden Kanten. in(x) : Liste der in x eingehenden Kanten. Das Hauptprogramm begin % Berechnung eines Sperrflusses auf LN der Tiefe k % for each x ∈ V do compute P O[x]; S[x] := 0 od; 14. Oktober 2008, Version 0.6 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus 36 for each h from 0 to k do Sh ← ∅ od; DEL := ∅; while LN is not empty do v := argminx∈V P O[x]; P O∗ := P O[v]; l := layer[v]; S[v] := P O∗ ; Sl := {v}; for h f rom l to k − 1 do for each x ∈ Sh do Forward (x, S[x], h) od od; S[v] := P O∗ ; Sl := {v}; for h f rom l step − 1 to 1 do for each x ∈ Sh do Backward (x, S[x], h) od od; Simplify (DEL) od end Die Prozedur Forward (x, S[x], h) Vor: x ∈ Vh hat Überfluss S. Ziel: Propagation von S Flusseinheiten in Schicht h + 1; nebenbei Entfernen saturierter Kanten sowie Aktualisierung aller Datenstrukturen und des aktuellen Flusses. procedure Forward (x, S[x], h) begin while S > 0 % iterativer Abbau des Überflusses bei Knoten x % do e = (x, y) := f irst edge f rom out (x); δ := min{S, c(e) − f (e)}; f (e) := f (e) + δ; if y 6∈ Sh+1 then add ytoSh+1 fi; S[y] := S[y] + δ; S := S − δ; if c(e) = f (e) then delete e from LN fi % Entfernen saturierter Kanten % od; delete x from Sh ; S[x] := 0; P O[x] := P O[x] − S; 14. Oktober 2008, Version 0.6 3.3 Berechnung von Sperrflüssen: Ein O(n2 )-Algorithmus if fi 37 (out(x) = ∅ and x 6= t)or(in(x) = ∅ and x 6= s) then add x to DEL % Falls s oder t zu DEL gehören, liegt ein Sperrfluss vor, der als Endresultat ausgegeben werden kann. Prozedur Simplify kann die Behandlung dieses Falles übernehmen. % end Die Prozedur Backward ist spiegelsymmetrisch. Prozedur Simplify (DEL) ist in O(n + # entfernte Kanten) zu implementieren (s. Übungen). Die Korrektheit des Algorithmus ergibt sich daraus, dass nur nutzlose Knoten und Kanten entfernt werden. Wenn LN leer ist, muss demzufolge ein Sperrfluss vorliegen. Zeitanalyse: Im Hauptprogramm finden maximal n Iterationen der while-Schleife statt. Der Aufwand pro Iteration ist O(n)+ “Gesamtaufwand für maximal n Forward- bzw. Backwardaufrufe”. Bei Forward kann eine nächste Iteration der while-Schleife nur gestartet werden, wenn die Kapazität der zuletzt benutzten Kante erschöpft, also auf Null gesetzt ist. In diesem Fall wird e entfernt. Der Aufwand für einen Forward-Aufruf beträgt somit O(1+# entfernte Kanten). Der Gesamtaufwand für die maximal n Forward-Aufrufe beträgt somit O(n + # entfernte Kanten). Satz 3.8 Ein Sperrfluss in LN kann in O(n2 ) Schritten berechnet werden. Folgerung 3.9 Ein maximaler Fluss in N kann in O(n3 ) Schritten berechnet werden. 14. Oktober 2008, Version 0.6 38 4 Graphalgorithmen: Das All-Pair-Shortest-Path Problem Wir setzen folgendes voraus, da es im Grundstudium vermittelt wurde: • Darstellung von Graphen durch Adjazenzmatrizen, Inzidenzmatrizen und Adjazenzlisten. • Tiefensuche (Depth First Search, DFS) • Breitensuche (Breadth First Search, BFS) • Algorithmen zur Berechnung von Minimalen Spannbäumen • Algorithmus von Dijkstra zur Berechnung von kürzesten Wegen von einem Knoten aus (Single Source Shortest Paths, SSSP) 4.1 Das All Pairs Shortest Paths Problem (APSP) Bei diesem Problem ist die Eingabe ein gerichteter, gewichteter Graph G = (V, E, w), w : E → R. Die Aufgabe besteht darin, für jedes Paar (u, v) ∈ V 2 die Länge eines kürzesten Weges von u nach v zu berechnen. 1. Möglichkeit Starte einen SSSP-Algorithmus von jedem Knoten v ∈ V aus. Da dieser die kürzesten Wege von v zu allen Knoten liefert, haben wir so das APSP-Problem gelöst. Falls G keine negativen Kreise enthält (z.B. falls alle Gewichte nicht-negativ sind), können wir Dijkstra’s Algorithmus anwenden und erreichen, je nach Implementierung der Priority Queue, Zeit O(|V |3 ), O(|V |·|E|·log(|V |) oder O(|V |2 log(|V |)+|V |·|E|). Falls negative Kantengewichte erlaubt sind, müssen wir |V | mal den Bellman-Ford Algorithmus starten, Zeit O(|V |2 |E|), also Zeit O(|V |4 ) für dichte Graphen. Geht es schneller? 4.2 Der Floyd-Warshall Algorithmus Sei G = (V, E, w) ein gerichteter, gewichteter Graph. Wir definieren: (k) dij := Länge eines kürzesten Weges von i nach j, der ausser i und j nur Kanten aus {1, . . . , k} benutzt. (Dabei vereinbaren wir: Für e 6∈ E ist w(e) := ∞). Die Resultate sind (n) dann die dij . (k) Wir erhalten folgende rekursive Definition der dij . w(i, j) k=0 (k) (k−1) (k−1) dij = (k−1) min di,j , di,k + dk,j k≥1 14. Oktober 2008, Version 0.6 4.2 Der Floyd-Warshall Algorithmus 39 Es ist einfach bei Eingabe von G als gewichtete Adjazenzmatrix aus obiger Rekursion einen (iterativen) Algorithmus mit Laufzeit O(|V |3 ) zu machen. Ebenso kann man ihn so modifizieren, dass er auch die kürzesten Wege, nicht nur ihre Länge ausgibt. Dieser Algorithmus ist ein Beispiel für dynamische Programmierung. Eine andere Methode zur Berechnung von APSP: (m) Sei aij := Dann gilt: Gewicht eines kürzesten Weges der Länge höchstens m von i nach j. (i, j) ∈ E w(i, j) (1) ∞ (i, j) 6∈ E aij = 0 i=j und für m ≥ 2 : (m) aij (m−1) (m−1) = min ai,j , min ai,k + w(k, j) 1≤k≤n (m−1) = min ai,k + w(k, j) 1≤k≤n (m) Wenn wir uns A(m) = (aij ) als n×n-Matrix vorstellen, ist A(1) = (w(i, j)) (mit w(i, i) = 0, und w(i, j) = ∞ für (i, j) 6∈ E). Für m ≥ 2 können wir A(m) als A(m−1) · A(1) berechnen, wobei in diesen “Matrizenmulitplikationen” die Multiplikation durch Addition und die Addition durch Minimumbildung ersetzt ist. Unser Ziel ist es A(n) = (A(1) )n zu berechnen. Eine Matrizenmultiplikation kostet nach der “Schulmethode” Zeit O(n3 ). Wenn wir naiv A(n) dadurch berechnen würden, dass wir immer A(m) als A(m−1) · A(1) berechnen, würden also Zeit O(|n|4 ) notwendig. Das geht besser durch iteriertes Quadrieren: Einfaches Beispiel: n = 2k . Berechnung A(n) (A(1) ) (1) A ← A(1) (2) for l = 1 to k (3) A ← A2 (4) return A Am Ende ist A(2 geführt. k) = A(n) berechnet, und nur k = log(n) Matrizenmultiplikationen aus- 14. Oktober 2008, Version 0.6 4.2 Der Floyd-Warshall Algorithmus 40 0 Falls n keine Zweierpotenz ist, können wir einfach A(n ) mit n0 = 2dlog(n)e die kleinste Zweierpotenz größer oder gleich n anstatt A(n) selbst, berechnen. Somit erhalten wir Laufzeit O(n3 log(n)). Häufig möchte man in anderen Zusammenhängen für eine n × n Matrix A die Matrix Ar 0 berechnen, und nicht Ar mit r0 = 2dlog(r)e . Das lässt sich ebenfalls mit einer Variante des iterierten Quadrierens erledigen: Sei (bk , . . . , b0 ) die Binärdarstellung von r. IteriertesQuadrieren((bk , . . . , b0 ), A) (1) if b0 = 0 then Z ← I (2) else Z ← A (3) initialisiere Z mit Einheitsmatrix (I) oder mit A (4) for l = 1 to k (5) A ← A2 (6) if bl =0 10 then Z ← Z · A Am Ende ist Z = Ar , es werden k = dlog(r)e Matrizenmultiplikationen (jeweils Zeit O(n3 )) und höchstens k = log(r) Matrizenadditionen (jeweils Zeit O(n2 )) benötigt. Somit ergibt sich für die Laufzeit O(n3 log(r)). 4.2.1 Berechnung Transitiver Hüllen Die transitive Hülle eines gerichteten Graphen G = (V, E) ist der Graph G∗ = (V, E ∗ ) mit [(i, j) ∈ E ∗ ⇔ ∃ (gerichteten) Weg von i nach j in G] Beide obigen Algorithmen können zur Berechnung der transitiven Hülle benutzt werden, falls wir Kantengewichte 1 für Kanten aus E, ∞ für Kanten nicht aus E einsetzen. (i, j) ∈ E ∗ gilt dann genau wenn der kürzeste Weg von i nach j Länge < ∞ hat. 14. Oktober 2008, Version 0.6 41 5 Entwurfsmethoden für Algorithmen Es gibt keinen “Meta-Algorithmus”, der zu einem Problem einen effizienten Algorithmus entwirft. Grundsätzlich gilt, dass man sich mit jedem Problem kreativ auseinandersetzen muß, um gute Algorithmen zu finden. (Das macht Algorithmenentwicklung zwar schwierig, aber auch interessant.) Auf der anderen Seite haben sich Methoden herausgeschält, die für größere Klassen von Algorithmen interessant sind. Eine solche Methode kennen wir schon, nämlich Divide & Conquer. Wir werden in diesem Kapitel zwei weitere Methoden kennenlernen, nämlich Dynamische Programmierung und Greedy-Algorithmen. 5.1 Dynamisches Programmieren Dynamische Programmierung ist eine Entwurfsmethode für Algorithmen, die für die Lösung eines Problems der Größe n alle “für diese Lösung relevanten Teilprobleme” der Größen 1, . . . , n − 1 berechnet, und daraus die Gesamtlösung zusammensetzt. Das sieht auf den ersten Blick ähnlich wie Divide & Conquer aus, allerdings mußten wir dabei immer wissen, an welcher Stelle wir das Problem aufteilen. 5.1.1 Problem 1: Matrizen-Kettenmultiplikation Hier sind die Matrizen M1 , . . . , Mn gegeben, wobei Mi eine pi−1 ×pi -Matrix ist, für natürliche Zahlen p0 , . . . , pn . Somit ist M1 · M2 · . . . · Mn definiert. Die Kosten einer Multiplikation einer p × q- mit einer q × r -Matrix sind nach der Schulmethode O(p · q · r), wir werden in folgenden Kosten p · q · r hierfür annehmen. Da die Matrizenmultiplikation assoziativ (Achtung: aber nicht kommutativ) ist, gibt es mehrere Möglichkeiten M1 · . . . · Mn zu berechnen. Beispiel: Betrachte M1 , M2 , M3 als (50 × 10)-, (10 × 20)-, (20 × 5) -Matrizen. Wir können sie auf zwei Arten multiplizieren: 1. (M1 · M2 ) · M3 Kosten: 50 · 10 · 20 + 50 · 20 · 5 = 15000 2. M1 · (M2 · M3 ) Kosten: 10 · 20 · 5 + 50 · 10 · 5 = 3500 Unser Ziel ist es, eine optimale Klammerung zu berechnen! Ein naiver Algorithmus probiert alle Klammerungen aus. Problem: Die Zahl k(n) der möglichen Klammerungen ist sehr groß: Es gibt (n − 1) Möglichkeiten für die obersten Klammern. Eine feste solche Möglichkeit läßt die Teilprobleme M1 , . . . , Mj bzw. Mj+1 , . . . , Mn zu klammern über, also gilt: 14. Oktober 2008, Version 0.6 5.1 Dynamisches Programmieren 42 k(1) = 1 n−1 X k(n) = k(j) · k(n − j) für n > 1 j=1 Die Lösung dieser Rekursion liefern die sog. Catalan-Zahlen C(n) genauer (C(n) = k(n+1)) 1 4n = Θ( Es gilt: C(n) = 2n · ). Somit ergibt sich exponentielle Laufzeit. n+1 n n3/2 Wir wollen nun einen besseren Algorithmus finden, indem wir das Prinzip der dynamischen Programmierung anwenden. Seien m(i, j) die minimalen Kosten, um Mi , . . . , Mj zu multiplizieren. Unser Ziel ist es = Θ(n2 ) viele m(i, j) mit 1 ≤ i ≤ j ≤ n zu berechnen. (Beachte: nun, alle n2 + n = n(n+1) 2 Unsere gesuchte Lösung ist m(1, n).) Diese m(i, j) möchten wir dabei in einer Reihenfolge berechnen, die es erlaubt, jedes m(i, j) mit geringem Aufwand aus dem vorher berechneten m(i0 , j 0 ) zu erzeugen. Dazu folgende Überlegung: Sei die optimale äußere Klammerung für Mi , . . . , Mj durch (Mi · . . . · Mk ) · (Mk+1 · . . . · Mj ) gegeben für ein k ∈ {i + 1, . . . , j − 1}. Mi · . . . · Mk ist eine pi−1 × pk -Matrix, Mk+1 . . . , Mj ist eine pk × pj -Matrix, also kostet die letzte Multiplikation pi−1 · pk · pj . Die Gesamtkosten sind dann: m(i, j) = m(i, k) + m(k + 1, j) + pi−1 · pk · pj . Allerdings wissen wir zu Beginn nicht, welches k optimal ist. Wir können jedoch folgende Aussage machen: m(i, i) = 0 und für j > i: m(i, j) = min {m(i, k) + m(k + 1, j) + pi−1 · pk · pj } i≤k<j In welcher Reihenfolge rechnen wir die m(i, j) aus? Zuerst alle m(i, i), dann alle m(i, i + 1), dann alle m(i, i + 2) usw. Für die m(i, i) benötigen wir je Zeit O(1)(m(i, i) = 0), für m(i, i + l) benötigen wir Zeit O(l) = O(n). Also: Gesamtzeit = O(n) + n2 · O(n) = O(n3 ). 5.1.2 Problem2: Optimale binäre Suchbäume Wir kennen binäre Suchbäume aus dem Grundstudium. In diesem Kapitel gehen wir davon aus, dass zu den Schlüsseln a1 < . . . < an Zugriffshäufigkeiten p1 , . . . , pn gegeben sind. Ziel 14. Oktober 2008, Version 0.6 5.1 Dynamisches Programmieren 43 ist es, einen Suchbaum für a1 , . . . , an zu konstruieren, so dass die mittlere Suchzeit n P ti · pi i=1 minimiert wird. Dabei ist ti die Tiefe von ai im Suchbaum. Wir stellen wieder fest, dass folgendes gilt: Falls ak die Wurzel im optimalen Suchbaum ist, können wir den optimalen Suchbaum dadurch konstruieren, dass wir als linken Ast unter ak einen optimalen Suchbaum für a1 , . . . , ak−1 und als rechten Ast unter ak eine für ak+1 , . . . an hängen. t(i, j) bezeichne die mittlere Suchzeit in einem optimalen Baum für ai , . . . , aj , d.h. t(i, j) = j P pl tl . l=i Dann gilt: t(i, j) = pi mini≤k<j {t(i, k − 1) + t(k + 1, j)} + w(i, j) mit w(i, j) = j X i=j j−i>0 pl l=i (Beachte: Der Term w(i, j) entsteht folgendermassen: Zusätzlich zu min{. . .} entstehen folgende Kosten: Für ak : Kosten pk ; für jedes l ∈ {i, . . . , j} \ {k}: Kosten pl (wegen der ersten Kante: ak → Wurzel des Suchbaums)). Wiederum kann man obiges einfach berechnen, indem die t(i, j) in der Reihenfolge: Alle t(i, i), dann alle t(i, i + 1), dann alle t(i, i + 2), usw. berechnet werden. Kosten: O(n3 ). Kann man die mittlere Tiefe eines Suchbaums durch eine geschlossene Formel in pi , . . . , pn ausdrücken? Ja, wir landen damit bei klassischen Ergebnissen der Informationstheorie. Wir betrachten dabei die folgende (nicht immer optimale) Konstruktion: m P Sei q = pl . l=1 Wähle die Wurzel ak so, dass gilt: k−1 X l=1 pl ≤ q/2 ≤ k X pl , l=1 fahre rekursiv in den beiden Teilbäumen nach der gleichen Regel fort. Beachte: Es gilt auch n P pl ≤ q/2. Zur Analyse gehen wir davon aus, dass p1 , . . . , pn eine Wahrscheinlichkeitsl=k+1 verteilung ist, also n P pl = 1. l=1 14. Oktober 2008, Version 0.6 5.1 Dynamisches Programmieren 44 Behauptung 5.1 Sei ak ein Element der Tiefe t im obigen Baum, der Teilbaum mit j P Wurzel ak verwalte ai , . . . , aj . Dann gilt: pl ≤ 2−t . l=i Beweis: Induktion nach t : t = 0 : Nur die Wurzel ak hat Tiefe 0 und pk = 1 ≤ 20 . Sei t > 0, ak habe Tiefe t, a0k sei Vorgänger von ak und der Suchbaum mit Wurzel a0k j P verwalte ai , . . . , aj . Nach Induktionsvoraussetzung ist pl ≤ 2−(t−1) . Der Subbaum mit l=i Wurzel ak verwaltet entweder ai , . . . , ak−1 oder ak+1 , . . . , aj . Nach Konstruktion gilt aber: k−1 X l=i j 1X pl ≤ pl 2 l=i Ind.V or. ≤ 1 −(t−1) ·2 = 2−t 2 und j X j 1X pl ≤ pl 2 l=i l=k+1 Ind.V or. ≤ 1 −(t−1) ·2 = 2−t . 2 2 Aus der Behauptung folgt insbesondere: Sei tk die Tiefe von ak , dann ist pk ≤ 2−tk , also tk ≤ − log(pk ), und somit folgt: Mittlere Suchtiefe im optimalen Baum ≤ Mittlere Suchtiefe im obigen Baum n n X X pk tk ≤ − pk log(pk ) = H(p1 , . . . , pn ) ≤ k=1 k=1 die Entropie von p1 , . . . , pn . Wir werden uns im nächsten Kapitel u.a. auch mit der Entropie beschäftigen und u.a. zeigen, dass die obige Schranke bis auf einen Faktor 1/ log2 (3) ∼ 0.631 scharf ist. Weitere Beispiele für dynamische Programmierung: Der Cocke-Younger-Kasami-Algorithmus für das Wortproblem kontextfreier Sprachen, weitere Optimierungsprobleme. Mögliche Effizienzverbesserungen: In unseren beiden Beispielen haben wir die Aufgabe, zur Berechnung eines Wertes m(i, j) ein Minimum der Form min {m(i, k) + m(k, j), . . . } i≤k≤j 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 45 oder min {m(i, k) + m(k + 1, j)), . . . } i≤k≤j auszuwerten. Sei ki,j das optimale k. Für unsere beiden Beispiele gilt: ki,j ≤ ki,j+1 . (Übung!) Dann gilt aber, dass wir bei der Berechnung von m(i, j + 1) nur min berechnen ki,j ≤k≤ki+1,j+1 müssen, also entstehen für m(i, j), m(i, j + 1), . . . , m(i, n) insgesamt nur Kosten O(n), d.h. die Laufzeiten für Matrizen-Kettenmultiplikation und optimale Suchbäume können auf O(n2 ) reduziert werden. Diese funktioniert aber nicht immer, z.B. nicht beim Algorithmus von Cocke, Younger, Kasami. 5.2 Greedy-Algorithmen Greedy heisst gierig, das beschreibt diesen Algorithmen-Typ recht gut. Er wird für Optimierungsprobleme benutzt, bei denen aus einer Menge eine optimale Teilmenge gewählt werden muß. Optimalität kann dabei auf viele Arten definiert werden. Ein Greedy-Algorithmus wird diese Teilmenge Element für Element aufbauen, und dabei immer “gierig” als nächstes dasjenige Element auswählen, das den Nutzen bezüglich der Zielfunktion am meisten erhöht. Ein Greedy-Algorithmus wird aber nie weiter vorausschauen, oder getroffene Entscheidungen zurücknehmen. Wir fragen uns: • Für welche Probleme liefern Greedy-Algorithmen optimale Lösungen? • Für welche beweisbar guten Lösungen? • Für welche schlechten Lösungen? Wir starten mit drei einfachen Beispielen für die drei oben genannten Fälle. 5.2.1 Bruchteil-Rucksackproblem: optimal Gegeben sind 2n + 1 positive Zahlen v1 , . . . vn , g1 , . . . , gn und G. Gesucht sind Zahlen n n P P a1 , . . . , an ∈ [0, 1], so dass ai gi ≤ G und ai vi maximal ist. i=1 i=1 Beachte: Falls die ai ∈ {0, 1} sein müssen, ist obiges Problem eine Variante des NP-vollständigen Rucksackproblems. Für obiges Bruchteil-Rucksackproblem sortieren wir zuerst, so dass vg11 ≥ vg22 ≥ . . . ≥ vgnn gilt. vgii ist der relative Wert pro Gewichtseinheit, es scheint also sinnvoll zu sein, lieber vi v als vj in die Lösung aufzunehmen, falls vgii > gjj ist. Der Greedy Algorithmus macht genau das: Es nimmt zuerst Objekt 1, dann Objekt 2 usw. immer vollständig, d.h. mit a1 = 1, a2 = 1, 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen usw. auf, bis das nächste Objekt ak die Restriktion 46 k P gi ≤ G verletzen würde. Hiervon i=1 nehmen wir nur den Bruchteil b ∈ (0, 1) auf, der dafür sorgt, dass k−1 P gi + bgk = G ist. i=1 Behauptung 5.2 Obiger Greedy-Algorithmus liefert optimale Leistung. Betrachte die lexikographisch erste optimale Lösung a1 , . . . , an . Warum existieren die? Falls für ein j > k gilt, dass aj > 0 ist, muß gelten: aj 0 < 1 für ein j 0 < k oder ak < b. (Denn sonst wäre die “≤ G”-Restriktion nicht erfüllt.) Dann könnten wir aber aj für ein genügend kleines > 0 um gj verringern, und aj 0 bzw. ak um g 0 bzw. gk erhöhen. Dadurch bleibt j das Grundgewicht der Lösung gleich, also ≤ G. Diese Lösung liefert wegen der Sortierung der vgii keinen schlechteren Zielfunktionswert, ist aber lexikographisch größer. Die Laufzeit wird durch das Sortieren dominiert, ist also O(n log(n)). 5.2.2 Rucksackproblem: sehr schlecht Wir benutzen den gleichen Algorithmus, erlauben aber nun nur ai ∈ {0, 1}. Damit ist die Lösung gegenüber der “Bruchteillösung” um b · gk kleiner. Wie schlimm kann das werden? Betrachte das einfache Beispiel n = 2, g1 = 1, g2 = G, v1 = 1, v2 = G − 1. Dann ist v1 =1> g1 v2 g2 = G−1 . G Unser Algorithmus würde also als Lösung v1 = 1 liefern. Die optimale Lösung ist aber offensichtlich v2 = G − 1. (Natürlich können wir hier auch keine optimale Lösung erwarten, da das Rucksackproblem NP-vollständig ist. Es gibt allerdings sehr viel bessere polynomielle Approximationsalgorithmen → Spezialvorlesung) 5.2.3 Bin Packing: nicht optimal, aber gut Gegeben sind n Objekte mit Gewichten a1 , . . . , an ∈ [0, 1]. Sie sollen auf möglichst wenige Bins (Kisten) verteilt werden, wobei jede Kiste höchstens Gewicht 1 aufnehmen kann. Bin Packing ist NP-schwer, also können wir keine schnellen optimalen Algorithmen erwarten. Wir können uns verschiedene Greedy-Algorithmen vorstellen. Wir haben Bins B1 , B2 , . . . zur Verfügung. First Fit: Plaziere hintereinander a1 , a2 , . . . , an und zwar ai immer in das Bin Bj mit kleinstem j, in das ai noch hinein passt. 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 47 Best Fit: ai wird jetzt in dem Bin platziert, in das es noch passt, das aber den geringsten Freiraum hat. Satz 5.3 (i) Beide Algorithmen benötigen höchstens um einen Faktor 2 mehr Bins als ein optimaler Algorithmus. (ii) Es gibt Beispiele, in denen obige Algorithmen um einen Faktor 5/3 mehr Bins benötigen als ein optimaler Algorithmus. Beweis zu (ii): Es sei n = 18m, a1 = . . . = a6m = 1 a18m = 12 + ε, für ε = 126 . 1 7 + ε, a6m+1 = . . . = a12m = 1 3 + ε und a12m+1 = . . . = Optimale Lösung: Je ein Element der drei Typen füllen eine Kiste exakt; also Opt = 6m. Lösung von F F : F F packt zuerst m Kisten jeweils mit 6 mal 71 + ε = 76 + 6ε. In diese Kisten passen keine weiteren Objekte. Dann kommen 3m Kisten mit je 2 mal 13 + ε. Die restlichen 6m Objekte brauchen je eine neue Kiste. Also: F F liefert Lösung 10m, ist also um Faktor 10 = 35 schlechter als Opt. Die obigen Beweise gelten analog für Best Fit. 2 6 [Im obigen Beispiel hätten wir eine optimale Lösung erhalten, wenn wir die Objekte zuerst monton fallend sortiert hätten. Die so entstehenden Algorithmen First Fit Decreasing und Best Fit Decreasing sind natürlich auch nicht optimal (Bin Packing ist ja NP-vollständig), ist. aber man kann zeigen, dass der Gütefaktor 11 9 (Bemerkung: (i) ist sogar mit dem Faktor 1.7 korrekt, allerdings ist der Beweis sehr länglich. (ii) gilt bereits mit Faktor 1.7-ε. (5/3 ≈ 1.667)).] 2 Beweis zu (i): Wir betrachten First Fit. Für Eingabe a1 , . . . , an sei OPT die optimale, FF die bei First Fit erzielte Zahl von Bins. Es gilt: 1. Opt ≥ n P ai i=1 2. Falls bei First Fit alle Kisten mehr als halb voll sind, ist F F < 2 · n P ai ≤ 2Opt. i=1 3. Falls es eine Kiste mit Beladung ε ≤ 1 2 gibt, sind alle F F − 1 anderen Kisten mit n P mindestens 1 − ε beladen. Also ist OP T ≥ ai ≥ (1 − ε)(F F − 1) + ε, also F F ≤ i=1 (OP T − ε/(1 − ε)) + 1 = (OP T + 1 − 2ε)/(1 − ε). Da dieser Term mit ε monoton wächst, und ε ≤ mum für ε = 21 an. Also: F F ≤ 2 · OP T . 14. Oktober 2008, Version 0.6 1 2 vorausgesetzt ist, nimmt er sein Maxi2 5.2 Greedy-Algorithmen 5.2.4 48 Prefixcodes nach Huffman: optimal Σ = {a1 , . . . an } sei ein Alphabet. Ein Präfixcode ist eine Abbildung c : Σ → {0, 1}∗ , so dass kein c(ai ) Präfix (Anfangstück) eines c(aj ) für j 6= i ist. [Beachte: Codeworte sind damit alle verschieden, sie dürfen verschiedene Länge haben. Präfixcodes haben gegenüber “normalen” Codes den Vorteil, dass wir ein Wort d1 d2 . . . dl ∈ Σ∗ einfach als c(d1 )c(d2 ) . . . c(dl ) codieren können, wir benötigen keine Trennsymbole und wissen trotzdem immer genau, wann die Codierung des nächsten Buchstabens beginnt.] Ein geschickt gewählter Code sollte häufig benutzten Buchstaben (z.B. dem ‘e’ in einem deutschen Text) kurze, und selten benutzten Buchstaben (z.B. ‘y’) längere Codeworte zuordnen. Formal: Seien p1 , . . . , pn Häufigkeiten für das Auftreten von a1 , . . .P an . Ein Präfixcode für Σ heißt optimal bzgl. p1 , . . . , pn , falls die mittlere Codewortlänge ni=1 pi (c(ai )) minimal unter allen Präfixcodes für (Σ, p1 , . . . , pn ) ist. Beispiel 5.4 Σ = {a, b, c, d, e, f }, • p(a) = 0.45, p(b) = 0.13, p(c) = 0.12, p(d) = 0.16, p(e) = 0.09, p(f ) = 0.05. • Ein möglicher Präfixcode: c(a) = 0, c(b) = 101, c(c) = 100, c(d) = 111, c(l) = 1101, c(f ) = 1100. • Mittlere Codewortlänge: 0.45 · 1 + 0.13 · 3 + 0.12 · 3 + 0.16 · 3 + 0.09 · 4 + 0.05 · 4 = 2.24. (Dieser Code ist tatsächlich optimal.) Der Huffman Algorithmus Wir gehen davon aus, dass p1 ≤ p2 ≤ . . . pn gilt. Nun bauen wir den Baum bottom up (von den Blättern startend) auf. Dazu werden wir zuerst die beiden Codeworte mit kleinster Häufigkeit, als a1 und a2 , zu einem Baum verschmelzen. a1 und a2 werden aus der Liste der Codewörter entfernt, und ein neues Element [a1 , a2 ] in sie eingefügt, und zwar gemäß ihres Gewichts p1 + p2 . Im Beispiel wird erzeugt, f, e gestrichen und [f, e] mit Gewicht 0.14 in die Liste einsortiert. Mit der neuen sortierten Liste von “Codewörtern” fahren wir auf gleiche Weise fort: Wir verschmelzen die beiden kleinsten “Codewörter” wieder zu einem Baum. Im Beispiel würden wir b und c verschmelzen zu b, c streichen, und [b, c] mit Gewicht 0.25 einsortieren. Somit haben wir nun folgende sortierte List vorliegen: [e, f ], d, [b, c], a, 0.14 0.16 0.25 0.45 Im nächsten Schritt würde also [e, f ] und d verschmolzen, und es entsteht 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 49 0 1 a 1 0 0 1 c 0 1 b 0 1 f e d Abbildung 7: Darstellung als Baum: Das Codewort von e ist am Weg zu e markiert: 1101 0 1 a 2 a1 mit Gewicht 0.3, [e, f ] und d werden gestrichen, [[e, f ], d] wird mit Gewicht 0.3, also zwischen [b, c] und a einsortiert. Für die Implementierung bietet sich zur Verwaltung der sortierten Liste offensichtlich eine Priority Queue an (wir müssen n mal minimale Elemente entfernen, jeweils logn Zeit) und 2n−1 mal Elemente einfügen (n mal, um a1 , . . . , an einzufügen, n−1 mal, um neue Elemente einzufügen, jeweils O(log(n)) Zeit). Somit benötigt der Algorithmus Zeit O(n log(n)). Korrektheit: Wir zeigen zuerst: Behauptung 5.5 Es gibt einen optimalen Codebaum, so dass a1 und a2 Geschwister sind. Beweis: Sei B ein optimaler Codebaum, ai und aj , i < j, zwei Geschwisterknoten auf dem untersten Level t von B. Was passiert mit der mittleren Codewertlänge in B, falls wir a1 und aj vertauschen? a1 befinden sich auf Level t0 ≤ t. Die mittlere Codewortlänge wird dann um folgenden Wert verändert: 0 f 14. Oktober 2008, Version 0.6 1 e 5.2 Greedy-Algorithmen 50 0 1 c b 0 1 d 0 e 1 f (p1 · t − p1 · t0 ) + (pi · t0 − pi · t) = (t0 − t) · (pi − p1 ) ≤ 0, | {z } | {z } ≤0 ≥0 d.h. der Baum wird nicht schlechter. Analoges folgt, wenn wir anschliessend a2 und aj vertauschen. Somit folgt die Behauptung. 2 Wir können nun zeigen: Satz 5.6 Der Huffman Algorithmus berechnet einen optimalen Präfixcode. Beweis: Induktion nach n. n = 1 : Das leere Wort wird berechnet und ist optimal. n>1: Sei wie oben p1 ≤ . . . ≤ pn . Nach Ind. Vor. berechnet die Huffman-Algorithmus einen optimalen Codebaum B für [a1 , a2 ], a3 , . . . , an , p1 + p2 , p3 , . . . , pn . Der Baum, den der HuffmanAlgorithmus für a1 , . . . , an , p1 , . . . , pn erzeugt, ist gerade der, den wir aus B erhalten, indem wir aus dem Blatt [a1 , a2 ] einen inneren Knoten mit Kindern a1 und a2 machen. Sei m(B) bzw. m(B 0 ) die mittlere Codewortlänge fur B bzw. B 0 . Dann gilt: m(B 0 ) = m(B) + p1 + p2 Wir müssen zeigen, dass dieses optimal ist. Dazu sei B 00 ein optimaler Codebaum für a1 , . . . , an , p1 , . . . , pn . Wegen der Beh. dürfen wir annehmen, dass in B 00 a1 und a2 Geschwister auf dem untersten Level sind. Ihr gemeinsamer Vorgänger heißt a. Wir entfernen nun aus B 00 die Blätter a1 und a2 und erhalten einen Codebaum B̄ für a, a3 , . . . , an , p1 + p2 , p3 , . . . , pn . Da B hierfür optimal war, gilt: m(B̄) ≥ m(B). Ausserdem ist offensichtlich m(B 00 ) = m(B̄) + p1 + p2 . Also folgt: 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 51 m(B 0 ) = m(B) + p1 + p2 ≤ m(B̄) + p1 + p2 = m(B 00 ). Da B 00 optimal ist, ist also m(B 0 ) = m(B 00 ), also ist auch B 0 optimal. 2 Beachte: Man kann zeigen (nicht sehr schwierig), dass H(p1 , . . . , pn ) ≤ m(B 0 ) ≤ H(p1 , . . . , pn ) + 1 gilt, falls p1 , . . . , pn eine Wahrscheinlichkeitsverteilung, d.h. pi ≥ 0 und P pi = 1 ist. Dabei ist H(p1 , . . . , pn ) = − n P i=1 pi log(pi )(= n P i=1 pi log( p1i )) die Entropie. [Die Codewortlänge für ai ist ziemlich genau log( p1i ). Genauer: Für die Codewortlängen n P t1 , . . . , tn gilt: 2−ti ≤ 1 (Kraft’sche Ungleichung). Dieses ist erfüllt, falls ti ≥ blog( p1i )c i=1 ist. Offensichtlich kann man einen Code mit Wortlängen dlog( p1i )e auch einfach angeben.] Die Aussagen H(p1 , . . . , pn ) ≤ optimale mittlere Codewortlänge ≤ H(p1 , . . . , pn ) + 1 ist Inhalt des berühmten Resultats von Shannon, des Noiseless Coding Theorem. 5.2.5 Wann sind Greedy-Algorithmen optimal? Sei E eine endliche Menge, U ein System von Teilmengen von E. (E, U ) heißt Teilmengensystem, falls gilt: (i) ∅ ∈ U (ii) Für jedes B ∈ U ist auch jede Teilmenge A von B in U . (Vererbungseigenschaft). B ∈ U heißt maximal, falls keine echte Obermenge von B in U ist. Das zu (E, U ) gehörige Optimierungsproblem besteht darin, zu gegebener Gewichtsfunktion w : E → R eine P maximale Menge B zu bestimmen, deren Gesamtgewicht w(B) = e∈B w(e) maximal ist (unter den Gesamtgewichten der maximalen Mengen). (Analoges läßt sich für Minimierung definieren.) Wir können z.B. das 0-1-Rucksackproblem wie folgt darstellen: Seien g1 , . . . , gn , G > 0 fest, E = {1, P. . . , n}. B ⊆ E ist in U , genau dann wenn gi ≤ G gilt. Ziel ist es, bei Gewichten w1 , . . . , wn ≥ 0 P i∈B ein B ∈ U zu finden, welches i∈B wi maximiert. Da ∅ ∈ U ist, und für B ⊆ U offensichtlich auch jede Teilmenge von B in U ist, gehört unser Optimierungsproblem zu einem Teilmengensystem. Zu einem Teilmengensystem (E, U ) und einer Gewichtsfunktion w arbeitet der kanonische Greedy-Algorithmus folgendermaßen: 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 52 KanonischGreedy((E, U )) (1) sortiere E, so dass w(e1 ) ≥ . . . ≥ w(en ) gilt (2) B ← ∅ (3) for k = 1 to n (4) if B ∪ {ek } ∈ U then B := B ∪ {ek } (5) return Lösung B Der Algorithmus aus 5.2.2 (Rucksack) arbeitet z.B. genau so. Wir wissen, dass er nicht optimal ist. Wir können auch zum MST Problem (Minimaler Spannbaum) ein Teilmengensystem definieren: E ist die Kantenmenge des zusammenhängenden Graphen G = (V, E), V = {1, . . . , n}, und U besteht aus allen Kantenmengen B ⊆ E, die keine Kreise enthalten. (Beachte: maximale Mengen in U sind Spannbäume!) Somit berechnet man zu Gewichten w(e), e ∈ E, einen Spannbaum, indem man ein maximales T ∈ U berechnet, welche w(T ) maximiert. Wir haben also ein zu einem Teilmengensystemen gehöriges Optimierungsproblem vorliegen. Der kanonische Greedy-Algorithmus, angewandt auf dieses Problem ist ihnen bekannt: Kruskals Algorithmus. Er ist optimal! Können wir einem Teilmengensystem “ansehen”, ob seine Optimierungsprobleme durch den generischen Greedy-Algorithmus optimal gelöst werden? Definition 5.7 Ein Teilmengensystem (E, U ) heißt Matroid, falls folgende “Austauscheigenschaft” gilt: Für alle A, B ∈ U, |A| < |B| gibt es x ∈ B\A mit A ∪ {x} ∈ U . Diese Eigenschaft gilt für das zum MST gehörige Teilmengensystem; genau diese wurde für den Beweis der Optimalität des Krusal-Algorithmus nachgewiesen: Betrachte zwei kreisfreie Teilmengen A, B von E, |A| < |B|. Seien V1 , . . . , Vk ⊆ V die Zusammenhangkomponenten von (V, A). (Beachte: Ein Baum auf n Knoten hat n − 1 Kanten, ein kreisfreier Graph auf n Knoten also höchstens n − 1 Kanten.) • Da |A| < |B| ≤ n − 1 ist, ist A kein Spannbaum, also k ≥ 2. • Da B kreisfrei ist, kann B in jeder Menge Vi höchstens |Vi | − 1 Kanten haben. • A hat in jedem Vi genau |Vi | − 1 Kanten. Also gibt es in B höchstens k P (|Vi |−1) = |A| viele Kanten, die innerhalb eines Vi verlaufen. i=1 Da |B| > |A| ist, gibt es in |B| also auch eine Kante e, die zwei verschiedene Vi verbindet. Diese kann aber in A keinen Kreis schließen, d.h. A ∪ {e} ∈ U . 14. Oktober 2008, Version 0.6 5.2 Greedy-Algorithmen 53 Dieses zeigt, das obiges System für MST ein Matroid ist. Das System für das Rucksackproblem ist kein Matroid, denn man kann sich leicht überzeugen, dass alle maximalen Mengen in einem Matroid gleich groß sind. Dieses gilt offensichtlich nicht für das Teilmengensystem des Rucksack-Problems. Satz 5.8 Sei (E, U ) ein Teilmengensystem. Der Kanonische Greedy-Algorithmus ist bzgl. jeder Gewichtsfunktion w : E → R optimal genau dann, wenn (E, U ) ein Matroid ist. Somit folgt aus obigen Überlegungen direkt, dass Kruskals Algorithmus für MST optimal ist, aber der kanonische Greedy-Algorithmus für Rucksack nicht optimal ist. Beweis des Satzes: 00 ⇐ 00 : Sei (E, U ) ein Matroid, w : E → R eine Gewichtsfunktion, w(e1 ) ≥ . . . ≥ w(en ), T 0 = {ei1 , . . . , eik } eine optimale Lösung. Annahme: Die vom Greedy-Algorithmus gefundene Lösung T = {ej1 , . . . , ejk } wäre nicht optimal, also w(T ) < w(T 0 ). Dann gilt w(ejl ) < w(eil ) für irgendein l. Betrachte das derartige minimale l. Sei A = {ej1 , . . . ejl−1 }, B = {ei1 , . . . , eil }. Die Austauschseigenschaft impliziert, dass es eir ∈ B gibt mit A0 = A ∪ {ejr } ⊆ U . Wegen der Minimalität von l gilt aber w(eir ) ≥ w(eil ) > w(ejl ). Somit hätte der Greedy-Algorithmus vor ejl bereits eir gewählt. Somit ergibt sich ein Widerspruch zur Annahme. 00 ⇒ 00 Annahme: Die Austauscheigenschaft gelte nicht, d.h. für bestimmte A, B ⊆ U, |A| < |B| gelten für alle e ∈ B, dass A ∪ {e} 6∈ U ist. Sei |B| = r. Betrachte folgende Gewichtsfunktion: e∈A r+1 r e ∈ B\A w(e) = 0 sonst Der Greedy-Algorithmus wird dann zuerst alle e ∈ A wählen. Von den folgenden e ∈ B\A kann er dann keines wählen, weil die A ∪ {e} für e ∈ B nicht in A sind. Demzufolge hat die Greedy-Lösung Gewicht (r + 1) · |A| ≤ (r + 1)(r − 1) = r2 − 1. Eine maximale Menge T , die B enthält, ist aber besser: w(T ) ≥ w(B) = r2 . Also ist der GreedyAlgorithmus nicht optimal, falls die Austauscheigenschaft nicht gilt. 2 14. Oktober 2008, Version 0.6 54 6 Berechnung des Medians, k-Selektion Wir haben beim Quicksort die Bedeutung des Medians kennengelernt. Wenn wir ihn als Split-Element benutzen, erreichen wir die best case Laufzeit. Wir werden ihn nun effizient berechnen, und allgemeiner ein effizientes Verfahren für die k-Selektion vorführen. (Daher n darf k auch von n abhängen, also etwa, wie beim Median, 2 sein.) Selektion(k ; a1 , ..., an ) (*berechnet k-t kleinstes Element von a1 , ..., an *) Falls n < 50 −→ M ergeSort sonst: (1) Zerlege a1 , ..., an in n5 Gruppen der Größe 5 (bzw. eventuell eine kleinere Gruppe) (2) Berechne in jeder Gruppe den Median. −→ Wir kennen die Gruppenmediane b1 , ..., bd n e und für jedes i 5 die Elemente der i-ten Gruppe, die kleiner bzw. größer als bi sind. (Median der Mediane berechnen) (3) Selektion( n5 /2 ; b1 , ..., bd n e ) 5 Ergebnis sei a. (4) Bestimme den Rang r von a, und D1 := {ai , ai < a}, D2 := {ai , ai > a}. (5) Falls k < r: Selektion(k, D1 ), falls k > r: Selektion(k − r, D2 ). Korrektheit: O.k. # Vergleiche: Für n < 50: n log(n) (vgl. MergeSort) n ≥ 50: Sei T (n) die maximale Zeit für Inputs aus {(k; a1 , ..., an ); k ∈ {1, ..., n}, ai ∈ IN }. # Vergleiche für: (1): 0 (2): n5 · 7 (7 Vergleiche reichen, um den Median von 5 Elementen zu berechnen → Übung) (3): T ( n5 ) (4): n − 1 (5): max{T (|D1 |), T (|D2 |)} Frage: Wie groß sind D1 , D2 ? 14. Oktober 2008, Version 0.6 55 alle b ∈ E1 sind ≤ a c c c 9 c (l − 1) Gruppenmediane ≤ a c E1 c c c c c c c c c c c c bi c ah c c c c c c c c c c c c n − l Gruppenc : 5 mediane > a c E2 c alle b ∈ E2 sind ≥ a Gruppe i ? Die Menge E1 (siehe Bild) enthält ≤ a (sie sind ≤ einem Gruppenmedian nur Elemente 1 n 3 bi , dieser ist ≤ a), und |E1 | ≥ 2 5 · 3 (≈ 10 n). 3 n). Analog: E2 enthält nur Elemente ≥ a, und |E2 | ≥ 21 n5 · 3 (≈ 10 Für n ≥ 50 sind |E1 |, |E2 | ≥ 14 n. Also: 14 n ≤ r ≤ 34 n, i.e. 3 |D1 | = r − 1 ≤ n 4 1 3 |D2 | = n − r ≤ n − n ≤ n 4 4 3 Zeit für (5) ≤ T ( n) 4 n 3 Insgesamt: T (n) ≤ T ( 5 ) + T ( 4 n) + 2.4 · n Behauptung: T (n) ≤ 48n Beweis: Induktion nach n → n ≤ 50 : T (n) n > 50 : T (n) ≤ n log(n) ≤ 48n T ( n5 ) + T ( 43 n) + 2.4n Ind.Vor. ≤ 48n · 51 + 48n · 43 + 2.4n ≤ ≤ 9.6n + 36n + 2.4n = 48n ≤ 48n. Satz 6.1 k-Selektion kann für beliebige (auch von n abhängige) Werte für k mit 48n Vergleichen durchgeführt werden. Korollar 6.2 Mit der Wahl des Medians als ausgewähltes Element erhält Quicksort eine worst-case O(n log(n)) Laufzeit. (Die Konstante ist allerdings sehr groß, diese Variante des Quicksort wird somit für die Praxis sehr schlecht.) 14. Oktober 2008, Version 0.6 56 7 Randomisierte Algorithmen Randomisierte (oder probabilistische) Algorithmen haben die Eigenschaft, dass es als Operation erlaubt ist, eine zufällige Zahl aus {0, . . . , m} zu erzeugen. (Random(m) liefert eine solche Zahl.) In der Praxis werden hierfür Pseudozufallszahlengeneratoren genutzt, die auf verschiedenste Art realisiert werden können, allerdings nicht im strengen Sinne “zufällige Zahlen” (was ist das überhaupt?!?) erzeugen. Wir werden im folgenden dieses Problem ignorieren, und davon ausgehen, dass Random(m) zufällig, gleichverteilt eine Zahl aus {0, . . . , m − 1} auswählt, und dass verschiedene Aufrufe von Random stochastisch unabhängige Ergebnisse liefern. Ein einfaches Spiel Gegeben: n geschlossene Dosen, in k ≤ n von ihnen ist eine Praline. Ziel: Finde eine Praline, aber öffne so wenige Dosen wie möglich. Ein deterministischer Algorithmus wird irgendeine Reihenfolge der Dosen festlegen, und sie in dieser Reihenfolge öffnen. Laufzeit: Best Case: 1 Worst Case: n − k + 1 RandomisierterAlgorithmus() (1) repeat (2) I ← Random(n) (3) öffne Dose I (4) until “gefunden” Laufzeit: Worst Case: ∞ Worst Case tritt mit Wahrscheinlichkeit 0 auf! Erwartete Laufzeit: P r(genau l Fehlversuche) = 14. Oktober 2008, Version 0.6 n−k n l · k n 57 E(#Fehlversuche) = ∞ X P r(genau l Fehlversuche) · l l=0 = = (∗) = = (*) gilt: Da für |x| < 1 gilt: ∞ P l ∞ X k n−k · ·l n n l=1 l−1 ∞ k n−k X n−k · ·l n n l=1 n −2 n−k k n−k · · 1− n n n −1 n−k k n−k · = n n k xl−1 · l = (1 − x)−2 l=1 Erwartete Zahl von Fehlversuchen: n−k k Beispiel 7.1 Sei k = ε · n für eine Konstante ε > 0. Sei δ > 0 gegeben. l P r(≥ l Fehlversuche) = nk = εl < δ für l > log1/ε (1/δ). Zahlenbeispiele: ε = 12 , δ = 2−20 (≈ 1 ) 1M io Dann reichen ≈ 20 Versuche mit Wahrscheinlichkeit 1 − 1M1 io aus. Eine einfache Anwendung: Eingabe: Ein Programm Q mit Eingaben aus Z, R oder Q. Wir wissen: Q wertet ein (uns nicht bekanntes!) Polynom p : R → R aus. Wir wissen: p hat Grad ≤ d. Ausgabe: Wahrheitswert von “p ≡ 0”? Test(Q) (1) index ← 0 (2) while index < l (3) Wähle zufälliges x ∈ {1, . . . , 2d}. (4) Starte Q mit Eingabe x. (5) if Q berechnet Wert 6= 0 then return p 6≡ 0 (6) index ← index + 1 (7) return p ≡ 0 Aufwand: O(l) + l· “Zeit für Q” (wir sagen: l Runden) Was berechnet Test? Falls p ≡ 0 ist, gibt es die korrekte Lösung aus. 14. Oktober 2008, Version 0.6 7.1 Grundbegriffe zu probabilistischen Algorithmen 58 Behauptung 7.2 Falls p 6≡ 0 ist, liefert Test die falsche Antwort nur mit Wahrscheinl lichkeit höchstens 12 . Beweis: Falls p 6≡ 0 ist, hat p höchstens d Nullstellen. Da wir x ∈ {1, . . . , 2d} zufällig wählen, erwischen wir mit obigem Spiel l Dosen (die l Versuche) und k = ε · n Pralinen (die Versuche, in denen wir eine Nicht-Nullstelle finden) für ε ≤ 12 . 2 Wir haben also einen Algorithmus mit einer festen Laufzeit (bei fester Eingabe) vorliegen, der allerdings mit kleiner Wahrscheinlichkeit eine falsche Ausgabe erzeugt, ein sogenannter Monte Carlo Algorithmus. Eine Variante: Eingabe: Ein Algorithmus Q, der ein Polynom p 6≡ 0 vom Grad d berechnet. Ausgabe: x mit p(x) 6= 0 Las-Vegas-Sucher() (1) repeat (2) wähle x zufällig aus {1, . . . , 2d} (3) if p(x) 6= 0 then x gefunden (4) until x gefunden (5) return x Dieser Algorithmus liefert immer das korrekte Ergebnis, falls er anhält. Allerdings ist nun l die Laufzeit T eine Zufallsvariable. P r(T > l) = P r(≥ l Fehlversuche) ≤ 21 Erwartete Laufzeit: E(T ) = 2 Algorithmen, die immer (vorausgesetzt sie halten) ein korrektes Ergebnis liefern, deren Laufzeit aber eine Zufallsvariable ist, heißen Las Vegas Algorithmen. (Ein weiteres bekanntes Beispiel für einen Las Vegas Algorithmus ist der randomisierte Quicksort Algorithmus.) 7.1 Grundbegriffe zu probabilistischen Algorithmen Deterministische Algorithmen zeichnen sich dadurch aus, dass Algorithmus und Eingabe die Rechnung (und damit ihre Länge und ihre Ausgabe) eindeutig festlegen. Nichtdeterministische Algorithmen erlauben dagegen bei fester Eingabe verschiedene Rechnungen mit verschiedenen Längen und verschiedenen Ergebnissen. Wenn wir uns vorstellen, dass wir nichtdeterministische Algorithmen so abarbeiten, dass wir jeweils zufällig eine der möglichen Nachfolgekonfigurationen auswählen, haben wir einen randomisierten Algorithmus. Ein randomisierter Algorithmus A ist ein Algorithmus, in dem wir zusätzlich erlauben, dass in einem Schritt von einer (im Algorithmus definierten) Menge M ein Element zufällig, gleichverteilt erzeugt wird. Beispiel: M {0, 1}, M = {0, . . . , k}, M = (0, 1). 14. Oktober 2008, Version 0.6 7.1 Grundbegriffe zu probabilistischen Algorithmen 59 Im Falle von M = {0, 1} sprechen wir häufig von “Münzwurf” (coin flip). Ein solcher Algorithmus kann nun abhängig von den Ergebnissen der Zufallschritte verschiedene Rechnungen ausführen. Jede Rechnung R taucht mit einer Wahrscheinlichkeit P r(R) auf. P r(R) ist dabei wie folgt definiert. Falls auf der Rechnung die Zufallsergebnisse a1 ∈ M1 , a2 ∈ M2 , as ∈ Ms auftauchen, ist P r(R) = |M11 | · . . . · |M1s | . P Klar: Falls C die Menge aller Rechnungen von A gestartet mit x ist, ist R∈C P r(R) = 1, d.h. wir haben eine Wahrscheinlichkeitsverteilung auf den Rechnungen aus C. Da die Rechnungen verschieden lang sind, haben wir kein deterministisches Maß für die Laufzeit, wir können aber die erwartete Laufzeit angeben. |R| bezeichne die Länge der Rechnung R. Erwartete Laufzeit von A gestartet mit x : ETP A (x) = P r(R) · |R| R∈C Erwartete Laufzeit im worst case: ETA (n) = maxx,|x|≤n ETA (x) Zuverlässigkeit der Laufzeitschranke: Wir wollen wissen: Wie sicher ist es, dass die Laufzeit T nicht wesentlich über der Erwartungszeit liegt? Mögliche Maße: • Varianz angeben • P r(TA (x) ≤ (1 + d)ETA (x)) ist klein. (siehe später) Was berechnet A? 1. Las Vegas Algorithmen: Jede endliche Rechnung von A gestartet mit x liefert das gleiche Ergebnis f (x), d.h. A macht keine Fehler. (Die Laufzeit ist weiterhin eine Zufallsvariable) 2. Monte Carlo Algorithmen: A darf Fehler machen. Wir müssen natürlich nun Einschränkungen an die Fehler machen: A berechnet f mit Fehler ε, falls für jede Eingabe x gilt: P r(A berechnet nicht f (x)) < ε Für Spracherkennung, d.h. f (x) ∈ {0, 1}, (Sprache L = f −1 (1)) unterscheiden wir zwei Typen: Einseitiger Fehler x ∈ L ⇒ P r(A sagt “x 6∈ L”) < ε x 6∈ L ⇒ A berechnet immer richtige Lösung (Für ε = 1 haben wir nichtdeterministische Algorithmen definiert) Zweiseitiger Fehler: x ∈ L ⇒ P r(A sagt “x 6∈ L”) < ε x 6∈ L ⇒ P r(A sagt “x ∈ L”) < ε. 14. Oktober 2008, Version 0.6 7.1 Grundbegriffe zu probabilistischen Algorithmen 60 Nun macht es keinen Sinn, ε ≥ 21 zu erlauben, da dann immer der triviale Algorithmus “Werfe Münze, gebe Ergebnis des Münzwurfs aus” funktioniert. Der Einfachheit halber (und oBdA, s.u.) gehen wir immer von ε ≤ 41 aus. 3. Amplifikation Wie robust sind randomisierte Algorithmen gegenüber • Wechsel von erwarteter Laufzeit zu worst case Laufzeit und • Veränderung der Fehlerwahrscheinlichkeit? worst case ↔ erwartete Laufzeit Für Las Vegas Algorithmen sind diese Maße deutlich unterschiedlich, sonst könnten wir sie deterministisch machen. Für Monte Carlo Algorithmen sieht das anders aus: Lemma 7.3 Sei A ein Monte Carlo Algorithmus für die Sprache L mit Fehlerwahrscheinlichkeit ε und ETA (n) = t(n). Sei δ > 0 so, dass ε+δ < 1 bzw. < 21 bei zweiseitigem Fehler. Dann gibt es einen Monte Carlo Algorithmus A∗ für L mit Fehlerwahrscheinlichkeit ε + δ und worst case Laufzeit 1δ t(n). Beweis: A∗ entsteht aus A, indem wir alle Rechnungen der Länge > 1δ t(n) nach 1δ t(n) Schritten abbrechen und irgendetwas ausgeben. A∗ hat natürlich worst case Laufzeit ≤ 1 t(n). Was passiert mit der Fehlerwahrscheinlichkeit? δ Die abgeschnittenen Rechnungen zusammen haben nur Wahrscheinlichkeit ≤ δ, denn sonst wäre die erwartete Zeit > δ · 1δ t(n) = t(n). Im schlimmsten Fall wären es lauter korrekte Rechnungen, die durch das Abschneiden fehlerhaft wurden. Aber auch dann erhöhen sie die Fehlerwahrscheinlichkeit um höchstens δ. 2 Änderung der Fehlerwahrscheinlichkeit Was passiert mit der Laufzeit von Monte Carlo Algorithmen, wenn wir die Fehlerwahrscheinlichkeit ε verändern? Bei einseitigem Fehler ist das sehr einfach zu analysieren: Wir lassen einen solchen Algorithmus mit Fehlerwahrscheinlichkeit ε bei Eingabe x l-mal unabhängig von einander laufen, und verwerfen x nur dann, wenn es in allen Versuchen verworfen wird. Lemma 7.4 Sei A ein Monte Carlo Algorithmus mit worst case Laufzeit t(n) und einseitiger Fehlerwahrscheinlichkeit ε. Dann hat der oben beschriebene Algorithmus A∗ worst case Laufzeit l · t(n) + O(l) und Fehlerwahrscheinlichkeit εl . Beweis: A∗ macht sicherlich genau wie A keine Fehler, falls x 6∈ L. Sonst verwirft er nur, wenn jeder der l unabhängigen Versuche einen Fehler macht. Dieses geschieht pro Versuch mit Wahrscheinlichkeit ε, als l-mal nur mit Wahrscheinlichkeit εl . 2 14. Oktober 2008, Version 0.6 7.1 Grundbegriffe zu probabilistischen Algorithmen 61 Beispiel 7.5 Falls A Fehlerwahrscheinlichkeit 12 und polynomielle Laufzeit nr hat, können wir die Fehlerwahrscheinlichkeit auf 21nl , l beliebig, reduzieren und noch polynomielle Laufzeit O(nr+l ) behalten. ( 21n ≈ 1M1 io für n = 20 !!!) Können wir für 2-seitigen Fehler eine ähnlichen Trick machen? Ja! Wir lassen den Algorithmus A mit worst case Laufzeit t(n) und Fehlerwahrscheinlichkeit ε < 14 wieder l mal laufen, und akzeptieren, falls mehr als die Hälfte der Versuche akzeptieren (majority vote). Lemma 7.6 A sei Monte Carlo Algorithmus für L mit worst case Laufzeit t(n) und 2seitiger Fehlerwahrscheinlichkeit ε ≤ 41 . Dann hat obiger Algorithmus A∗ für L die Laufzeit l · t(n) + O(l) und Fehlerwahrscheinlichkeit (4ε)l/2 . l/2 l Beweis: P r(A∗ ist bei Eingabe x ≥ l/2 mal fehlerhaft) ≤ l/2 · ε ≤ 2l εl/2 = (4ε)l/2 2 Beispiel 7.7 Wir erzielen einen ähnlichen Effekt bei polynomiellen Algorithmen wie schon vorher für einseitige Fehler gezeigt. 7.1.1 Randomisierte Komplexitätsklassen RP = {L : ∃ polynomiellen Monte Carlo Algorithmus für L mit einseitigem Fehler < 21 }. 1 ( 12 könnte auch durch 2poly(n) ersetzt werden, siehe oben.) BPP = {L : ∃ polynomiellen Monte Carlo Algorithmus für L mit zweiseitigem Fehler < 41 }. 1 ( 14 ist das gleiche wie 2poly(n) .) PP = {L : ∃ polynomiellen Monte Carlo Algorithmus für L mit zweiseitigem Fehler < 21 }. ZPP = {L : Es gibt polynomiellen Las Vegas Algorithmus für L}. ZPP, RP und BPP sind von praktischem Interesse, weil sie Algorithmen mit sehr kleinen, vernachlässigbaren Fehlerwahrscheinlichkeiten behandeln. Spannende offene Probleme in der Komplexitätstheorie (i) RP = coRP ?? (ii) RP ⊆ N P ∩ coN P ? (würde aus (i) folgen, da coRP ⊆ coN P ) (iii) BP P ⊆ N P ? Eine einfache Charakterisierung für ZP P : Es gilt ZP P = RP ∩ coRP (Beweis: Übung) 14. Oktober 2008, Version 0.6 7.2 Einige grundlegende randomisierte Algorithmen 62 7.2 Einige grundlegende randomisierte Algorithmen 7.2.1 Verifikation von Polynom-Identitäten und Anwendungen Wir haben schon am Eingangsbeispiel gesehen, wie man einfach testen kann, ob ein Polynom p : Z → Z identisch Null ist (bzw.: ob zwei Polynome p, q : Z → Z identisch sind; teste p − q ≡ 0), indem man p nur an wenigen, zufällig gewählten Stellen auswertet. Wir zeigen nun ein ähnliches Resultat für Polynome in mehreren Variablen. Satz 7.8 (Schwarz-Zippel) Sei F ein Körper, p ∈ F(x1 , . . . , xn ) ein Polynom vom Grad d, S ⊆ F eine endliche Menge. Falls r1 , . . . , rn ∈ S unabhängig, zufällig, gewählt sind, gilt: P r(p(r1 , . . . , rn ) = 0 | p 6≡ 0) ≤ d |S| . Beweis: Induktion nach n. n = 1: Dann hat p höchstens d Nullstellen, schlimmstenfalls alle in S. Somit wird Nullstelle d mit Wahrscheinlichkeit ≤ |S| gezogen. d P n > 1 : Schreibe p : Fn → F als p(x1 , . . . , xn ) = xi1 · pi (x2 , . . . , xn ). i=0 Dann hat pi nur n − 1 Variablen und Grad ≤ d − i. Sei k maximal mit pk 6≡ 0. (k existiert, da p 6≡ 0). Für zufällige r2 , . . . , rn ∈ S gilt nach Induktionsvoraussetzung: . (i) P r(pk (r2 , . . . , rn ) = 0) ≤ d−k |S| Sei nun p 6≡ 0 und k, pk wie oben. Betrachte nun festes r2 , . . . , rn mit pk (r2 , . . . , rn ) 6= 0. Das Polynom q(x1 ) = p(x1 , r2 , . . . , rn ) hat Grad k und eine Variable. Bei zufälliger Wahl k von r1 ∈ S ist also P r(q(r1 ) = 0) ≤ |S| . Insbesondere folgt: k (ii) P r(p(r1 , . . . , rn ) = 0 | pk (r2 , . . . , rn ) 6= 0) ≤ |S| . Beachte: Für Ereignisse E1 , E2 gilt P r(E1 ) ≤ P r(E1 |ε̄2 ) + P r(E2 )). Also gilt der Satz wegen (i) und (ii). 2 7.2.2 Perfekte Matchings in Bipartite Graphen G = (U, V, E) ist ein bipartiter Graph mit Knotenmenge U ∪ V (U ∩ V = ∅), falls alle Kanten zwischen U und V verlaufen. Ein Matching in G ist ein Subgraph vom Grad 1. In einem perfekten Matching ist jeder Knoten inzident zu einer Matchingkante. Frage: Wie entscheiden wir, ob ein bipartiter √ Graph ein perfektes Matching enthält? Variante 1: Flußalgorithmen → Zeit O(m n). Variante 2: Satz 7.9 (Edmonds) A sei die n × n Matrix, die aus G = (U, V, E) entsteht durch 14. Oktober 2008, Version 0.6 7.2 Einige grundlegende randomisierte Algorithmen ( xij Aij = 0 63 (ui , vj ) ∈ E sonst. Betrachte Polynom Q(x1,1 , . . . , xn,n ) := det(A). G hat ein perfektes Matching ⇔ Q 6≡ 0. P Beweis: det(A) = sgn(Π) · A1,Π(1) · . . . · An,Π(n) Π∈Sn Jedes perfekte Matching können wir durch eine Permutation Π ∈ Sn beschreiben, es enthält die Kanten (ui , vΠ(i) ), i = 1, . . . , n. Genau die Π ∈ Sn die ein Matching beschreiben, leisten einen Beitrag zu obigem Polynom, d.h. X det(A) = sgn(Π) · A1,Π(1) · . . . · An,Π(n) Π∈Sn Π M atching Falls mindestens ein Matching existiert, ist mindestens ein Summand ungleich Null. Da jeder Sumand durch ein anderes Produkt von Variablen bestimmt ist, ist dann Q 6≡ 0 2 Q ist Polynom von Grad n. Falls wir also zufällig r1 , . . . , rn ∈ {1, . . . , k · n} wählen und testen, ob Q(r1 , . . . , rn ) = 0 ist, gilt: P r(Q(r1 , . . . , rn ) = 0 | q 6≡ 0) ≤ k1 . Satz 7.10 Obiger Monte Carlo Algorithmus mit einseitigem Fehler < ein bipartiter Graph ein perfektes Matching enthält. 1 k entscheidet, ob Wir bemerken, dass dieser Algorithmus zu den auf Flußtechniken basierenden nicht konkurenzfähig ist, da er eine Determinante ausrechnen muß (Zeit = ˆ Zeit für Matrixmultiplikation = ˆ n2.376 (Coppersmith/Winograd). Der deterministische Algorithmus benötigt zwar für dichte Graphen Zeit O(n2.5 ), ist in der Praxis aber viel schneller. Der randomisierte Algorithmus ist jedoch einfach parallelisierbar. 7.2.3 Perfekte Matchings in beliebigen Graphen G = (V, E) sei nun beliebiger ungerichteter Graph V = {1, . . . , n}. Satz 7.11 (Tutte) Betrachte zu G folgende n × n Matrix A. Zu jeder Kante e = (i, j), i < j, sei eine Variable xi,j assoziert. Die Matrix A hat dann Einträge: {i, j} 6∈ E 0 Ai,j = xi,j i < j, {i, j} ∈ E −xj,i i > j, {i, j} ∈ E G hat perfektes Matching ⇔ detA 6≡ O. 14. Oktober 2008, Version 0.6 7.2 Einige grundlegende randomisierte Algorithmen 64 Beweis: Übung. 2 Analog zu Satz 7.3 erhalten wir: Satz 7.12 Falls wir ,,det(A(r1 , . . . , rn )) = 0” für zufällige r, . . . , rn ∈ {1, . . . , k · n} testen, erhalten wir einen Monte Carlo Algorithmus mit einseitigem Fehler < k1 , der entscheidet, ob G ein perfektes Matching enthält. 7.2.4 Effiziente Tests für “p(x) = 0” Obige Verfahren basiern auf der Auswertung von Straight Line Programmen, die Polynome beschreiben. Wir haben bisher hierfür als Laufzeit die Zahl der auszuführenden Operationen betrachtet. Da diese Operationen aber Multiplikationen enthalten, können wir in kurzer t Zeit sehr lange Operanden erhalten. Z.B. können wir 22 in t Schritten berechnen (t-fach iteriertes Quadrieren). p Bemerkung 7.13 Sei A ∈ Mn,n ({−s, . . . , s}). Dann gilt |det(A)| ≤ ( (n) · s)n . In unserem obigen Verfahren haben wir Determinaten für Einträge ≈ k ·n ausgewertet, also können (tatsächlich) Werte der Größe nΘ(n) , also Zahlen mit binärer Länge Θ(n log n) als (Zwischen)- Ergebnis auftauchen. Wir werden sehen, daß es auch reicht, mit nur O(log(n)) langen (Zwischen-) Ergebnissen zu arbeiten. Lemma 7.14 Die Zahl der verschiedenen Primteiler einer Zahl k ist höchstens log(k). Beweis: Primteiler sind ≥ 2, das Produkt der verschiedenen Primteiler von k ist ≤ k. 2 Lemma 7.15 Sei k > 0, p prim, k nicht Vielfaches von p. Dann ist k mod p 6= 0. Satz 7.16 Sei S ein Straight Line Programm, in dem nur Konstanten a, |a| ≤ k, auftauchen, der Länge t. S berechne Polynom Q. Sei x ∈ Zn und |x|∞ ≤ l. Sei p zufällig gewählte Primzahl. p ≤ R0 = s · (2t log(k · l)). Dann gilt: Falls Q(x) 6= 0 ist, ist Q(x) mod p 6= 0 mit Wahrscheinlichkeit ≥ (1 − 1s ). t t t t Beweis: Es gilt: |Q(x)| ≤ k 2 ·l2 . Somit hat |Q(x)| höchstens R = log(k 2 ·l2 ) = 2t ·log(k·l) viele Primteiler. In {1, . . . , R0 ) sind aber mindestens s · R viele Primzahlen. Also folgt: Mit Wahrscheinlichkeit ≤ 1s wird Primteiler p von Q(x) ausgewählt. Deshalb wird mit Wahrscheinlichkeit ≤ 1s das Ergebnis Q(x) mod p = 0 berechnet. 2 t Dieser Algorithmus arbeitet mit Operanden der Länge ≈ t, anstatt 2 !! (Auswertung der SLP nutzt nun Arithmetik im Körper Zp .) Speziell für das nicht. Aber da wissen wir: √ Determinatenberechnung √ reicht n 0 n Q(x) ≤ ( ns) , also R ≈ log(s( ns) ) ≈ n log n reicht. Also haben Operanden Länge höchstens log(R0 ) ≈ log(n). 14. Oktober 2008, Version 0.6 7.2 Einige grundlegende randomisierte Algorithmen 7.2.5 65 Quicksort Input S, |S| = n 1. Wähle zufälliges x ∈ S (Splitelement) 2. Vergleiche jedes y ∈ S mit x, bilde S1 = {y ∈ S, y < x}, S2 = {y ∈ S, y > x}. 3. Sortiere Rekursiv S1 und S2 . Ausgabe: Sortiertes S1 , x, sortiertes S2 . Erwartete Zahl von Vergleichen? Beachte: Zwei Elemente x, y werden höchstens einmal verglichen, nämlich wenn eins von ihnen als Splitelement genutzt wird, und das andere in der zu splittenden Menge ist. S(i) sei das Element mit Rang i aus S. Für i < j sei Xij die Zufallsvariable ( 1 S(i) und S(j) werden verglichen Xij = 0 sonst Erwartete Zahl von Vergleichen: n P P PP E( Xij ) = E(Xij ). i=1 j>i Sei pij die Wahrscheinlichkeit, dass S(i) und S(j) verglichen werden, d.h. pij = P r(Xij = 1) ⇒ E(Xij) = pij · 1 + (1 − pij ) · 0 = pij Wir müssen pij bestimmen. Betrachte Rechnung von Quicksort bis eine Partition S 0 = {S(a) , . . . , S(b) } a ≤ i, b ≥ j erreicht ist, und im nächsten Schritt ein Split-Element S(r) mit i ≤ r ≤ j gewählt wird (also im nächsten Schritt S(i) und S(j) separiert werden). Wann wird S(i) mit S(j) verglichen? Kann nur in diesem Schritt passieren, und zwar genau dann, wenn r = i oder r = j ist. 14. Oktober 2008, Version 0.6 7.2 Einige grundlegende randomisierte Algorithmen 66 Somit gilt: pij = Also f olgt :E(# Vergleiche ) = 2 j−i+1 n−1 XX i=1 j>i = 2 j−i+1 n−1 n−i+1 X X 2 i=1 k=2 n−1 X n X ≤ 2 i=1 k=2 k 1 k = 2(n − 1) · (Hn − 1) ≤ 2(n − 1) ln(n) Hn – Harmonische Zahl (Hn ≤ ln(n) + 1) Satz 7.17 Quicksort benötigt eine erwartete Anzahl von ≤ 2(n − 1)ln(n) viele Vergleiche. Ein anderer Beweis: Sei T (n) die erwartete Anzahl Vergleiche von n Zahlen zu sortieren. Dann gilt: T (1) = 0, und für n > 1: n 1X T (n) = [T (r − 1) + T (n − r)] + (n − 1) n r=1 Wir erhalten durch Lösen der Rekursion das gleiche Ergebnis. 14. Oktober 2008, Version 0.6 2 67 8 Ein randomisiertes Wörterbuch: Skip-Listen Der abstrakte Datentyp ,,Wörterbuch” verwaltet eine Teilmenge S einer geordneten Menge (z.B. N) so, dass effizient die Operationen Search, Delete und Insert unterstützt werden. Aus dem Grundstudium sind weitere Datenstrukturen wie rot-schwarz Bäume, AVLBäume oder 2-3-Bäume bekannt, die jeweils O(n) Platz und worst case Zeit O(log(n)) pro Operation benötigen (n = |S|). Die Datenstrukturen werden in der Praxis häufig benutzt, ihre Implementierung ist jedoch sehr aufwendig. Wir stellen nun eine relativ neue, randomisierte Datenstruktur für Wörterbücher vor, die sehr einfach ist, und erwartete Laufzeit O(log n) pro Operation aufweist. Sie hat in den letzten Jahren begonnen, auch in der Praxis immer häufiger eingesetzt zu werden, da sie sich auch dort als sehr effizient erweist, und sehr einfach zu implementieren ist. Eine Skipliste für Elemente X = {x1 < ... < xn } besteht aus linearen Listen L0 , L1 , ..., Lh . Jede Liste verwaltet eine Teilmenge Xi von X in aufsteigend sortierter Reihenfolge. Welches Element in welchen Listen vertreten ist, wird durch die Höhen h(xi ) ≥ 1 der Elemente bestimmt: xi ist in den Listen L0 , ..., Lh(xi )−1 gespeichert. Insbesondere gilt also: X0 = X, X0 ⊇ X1 ⊇ ... ⊇ Xh . Beispiel: L3 L2 −∞ L1 5 L0 - - 8 - - - 10 - 14 - - 16 - - - - 21 - - - - ∞ - 25 - Eine Skipliste für X = {5, 8, 10, 14, 16, 21, 25} mit h = 4, h(5) = 2, h(8) = 3, h(10) = 1, h(14) = 3, h(16) = 1, h(21) = 4, h(25) = 2. Search(x): i ← h − 1, Lef t ← −∞, gefunden“ = f alse ” Solange i ≥ 0 und nicht gefunden“: ” Suche in Li , startend in Lef t, bis zuerst ein y ≥ x gefunden ist. Falls x = y, dann gefunden“ = true; gebe x aus. ” sonst: Lef t → P rev(y); i ← i − 1. /* Falls x ∈ X gilt, ist x ≥ Lef t. */ Falls gefunden“ = f alse, gebe Fehlermeldung aus. ” 14. Oktober 2008, Version 0.6 68 Wegen des Kommentars ist die Suche offensichtlich korrekt. Beispiele: Für Search(10) wird folgendes durchlaufen: In L3 : −∞, 21 In L2 : −∞, 8, 14 In L1 : 8, 14 In L0 : 8, 10 Für Search(9) würden die gleichen Elemente durchlaufen, allerdings dann eine Fehlermeldung ausgegeben. Für alle x ∈ (5, 8) werden genau die gleichen Elemente durchlaufen, danach wird die Fehlermeldung ausgegeben. Insert(x): Wir müssen entscheiden, in welche Listen L0 , ..., Lh(x)−1 x eingefügt wird, d.h. wir müssen h(x) berechnen. Hierfür benutzen wir den Zufallszahlengenerator: Wir simulieren Münzwürfe“, d.h. benut” zen einen Zufallszahlengenerator, der uns (Folgen von unabhängigen) Werte(n) aus {0, 1} liefert. Dabei liefert er jeweils eine 0 mit Wahrscheinlichkeit 21 . Wir bestimmen h(x) als die Anzahl der Münzwürfe, bis das erste Mal eine 1 erscheint. Die Wahrscheinlichkeit für Höhe h ist also gleich der Wahrscheinlichkeit, dass zuerst h − 1 mal 0, und dann eine 1 auftritt. Also ist die Wahrscheinlichkeit ( 12 )h . Damit gilt für die erwartete Höhe E(h): E(h) = P (h = 1) · 1 + P (h = 2) · 2 + ... h ∞ X 1 h· = =2 2 h=1 Wir fügen nun x ein, indem wir zuerst Search(x) durchführen und uns in den Listen Lh(x)−1 , ..., L0 die Position speichern, wo x hingehört. Falls x bereits in X ist, aktualisieren wir nur sein Datenfeld. Sonst fügen wir x in Lh(x)−1 , ..., L0 ein. Da wir uns die Positionen gemerkt haben, kostet Einfügen pro Liste konstante Zeit. Also benötigt Insert(x) Zeit O(Zeit für Search(x)). Delete(x): Wir starten wieder mit Search(x). Falls wir x in Lj finden, ist j = h(x)−1 und wir streichen x in Lh(x)−1 , ..., L0 . Da wir mit der Position von x in Lh(x)−1 auch seine Positionen in den anderen Listen kennen, sind die Kosten pro Streichen aus einer linearen Liste konstant, somit sind die Gesamtkosten O((Zeit für Search(x)) + h(x)). Beachte: Diese Kosten sind höchstens die Kosten für Search(z) für ein z ∈ (xi , xi+1 ), wobei x = xi ist. (Also ist z nicht in X, und sein Suchweg ist zuerst der gleiche wie der für x, geht dann aber bis zu L0 herunter.) Somit reicht es, die Laufzeitanalyse für Search(z) für ein z ∈ / X durchzuführen. 14. Oktober 2008, Version 0.6 69 Lemma 8.1 a) Die erwartete Höhe H(n) einer Skip-Liste für n Elemente ist O(log(n)). b) Die erwartete Zahl von Zeigern in einer Skip-Liste mit n Elementen ist höchstens 2n + O(log(n)). zu a) In X0 sind alle Elemente. In X1 ist jedes Element aus X0 mit Wahrscheinlichkeit 21 , also erwarten wir hier 12 n Elemente. X2 ist jedes Element aus X1 mit Wahrscheinlichkeit In 1 1 1 1 , also erwarten wir hier 2 · 2 n = 4 n Elemente. 2 Allgemein: In Li erwarten wir 21i · n Elemente. In Lblog(n)c+2 erwarten wir damit weniger als 1, also null Elemente. Also ist H(n) ≤ blog(n)c + 1. zu b) Wir haben schon gesehen, dass die erwartete Höhe eines Elements 2 ist. Somit ist die erwartete Zahl von Zeigern, die aus Elementen heraus zeigen ebenfalls 2. Je ein weiterer Zeiger ist für jede der O(log(n)) Listen am Anfang nötig. Satz 8.2 Eine Skip-Liste für n Elemente hat erwartete Größe O(n) und erwartete Zeit pro Operation O(log(n)). Beweis: Wie oben gesagt, reicht es eine erfolglose Suche nach einem z ∈ / X zu analysieren. Wir nutzen eine sehr elegante Beweismethode, der Rückwärtsanalyse. D.h. wir schauen uns die Suche rückwärts an: Am Ende ist in L0 die Lücke xi , xi+1 gefunden, in die z hineingehört. Wie viele Elemente haben wir in L0 dafür anschauen müssen? In L1 haben wir zwei aufeinander folgende Elemente a und b gefunden, mit a < z < b. Natürlich sind a, b auch in L0 , und wir suchen in L0 höchstens alle Elemente y ∈ X0 mit a ≤ y ≤ b. Beachte: Jedes solche y ist mit Wahrscheinlichkeit 12 auch in L1 ! Somit erwarten wir, dass in L0 im Durchschnitt höchstens zwei Elemente zwischen a und b liegen, wir also nur erwartete 4 Elemente in L0 durchsuchen. Analoges gilt für alle anderen Levels. Also sind die erwarteten Kosten für die Suche nach z 4 · H(n) = O(log(n)). Die Schranke für die Größe folgt direkt aus Teil b) des Lemmas. 14. Oktober 2008, Version 0.6 2 70 9 Berechnung minimaler Schnitte in Graphen Wir betrachten einen gewichteten Graphen G = (V, E, w), w : E → N. Ein Schnitt in G ist eine Menge S ⊆ E mit der Eigenschaft, dass (V, E \ S) nicht zusammenhängend ist. Wir wollen nun in G einen minimalen Schnitt, d.h. einen Schnitt mit minimalem Gewicht berechnen. 9.1 Ein sehr einfacher Algorithmus Eine Kontraktion einer Kante e von G identifiziert die beiden Endpunkte a, b von e zu einem Punkt c, d.h. die Kante zwischen a und b verschwindet, Kanten (i, a) und (i, b) werden zu einer Kante (i, c) mit Gewicht w(i, a) + w(i, b). Durch eine Kontraktion wird die Knotenmenge von G um eins verkleinert. MinCut() P (1) w(E) = e∈E w(e) (2) for i = 1 to n − 2 w(e) (3) wähle zufällige Kante e von G mit P r[e] = w(E) (4) Kontrahiere e (5) G ← kontrahierter Graph (6) w(E) ← w(E) − w(e) (7) /∗ Nun hat G hat nur noch zwei Knoten a, b ∗/ (8) Gebe w(a, b) aus. Behauptung 9.1 G0 gehe aus G durch eine Kontraktion hervor. Dann ist der MinCut von G0 nicht kleiner als der von G. Beweis: Betrachte Cut S in G0 . Wenn man die Kontraktion rückgängig macht, dann ist der der Cut auch ein Cut in G. 2 Daraus ergibt sich, dass der obrige Algorithmus einen Cut in G liefert. Wir können den Cut berechnen, indem wir Schritt für Schritt die Kontraktionen rückgängig machen und uns die so wieder entstehenden Kanten aus (a, b) merken. Frage: Mit welcher Wahrscheinlichkeit ist dieser ein MinCut von G? Lemma 9.2 Es sei C ein fester MinCut von G. P r(Algorithmus erzeugt C) ≥ 2 n(n − 1) Beweis: Sei k = w(C). Dann ist w(E) ≥ k2 · n, denn sonst gäbe es eine Knoten, deren inzidente Kanten ein Gewicht kleiner k haben. Dieses ergäbe somit einen kleineren Schnitt. Es gilt: Der Algorithmus erzeugt C ⇔ Kanten aus C werden nie kontrahiert. 14. Oktober 2008, Version 0.6 9.1 Ein sehr einfacher Algorithmus 71 Sei G0 = (V 0 , E 0 , w0 ) der Graph nach einer Kontraktion. εi bezeichne das Ereignis, dass in einer Runde i (1 ≤ i ≤ n − 2) keine Kante aus C gewählt wird. w(C) w(E) k ≥ 1− k ·n 2 2 = 1− n P r(ε1 ) = 1 − w0 (C 0 ) w0 (E 0 ) k ≥ 1− k · (n − 1) 2 2 = 1− n−1 ! i−1 \ 2 P r εi | εj = 1 − n−i+1 j=1 P r(ε2 |ε1 ) = 1 − Daraus folgt: n−2 \ P r(C überlebt) = P r ! εi i=1 = P r(εi ) · P r(ε2 |ε1 ) · P r(ε3 |ε1 ∧ ε2 ) · · · · · P r(εn−2 |ε1 ∧ · · · ∧ εn−3 ) n−2 Y 2 ≥ 1− n−i+1 i=1 = n−2 Y i=1 n−i−1 n−i+1 n−2 n−3 n−4 3 2 1 · · · ··· · · · n n−1 n−2 5 4 3 2 = n(n − 1) = 2 MinCut kann in O(n ) ausgeführt werden. Die Erfolgswahrscheinlichkeit ist natürlich nicht zufriedenstellend. oft aus und wähle den kleinsten so erhaltenen Cut Aber: Führe den Algorithmus t · n(n−1) 2 als Ergebnis aus. 2 P r(Cut ist nicht MinCut) = 14. Oktober 2008, Version 0.6 2 1− n(n − 1) t· n(n−1) 2 ≤ e−t 9.1 Ein sehr einfacher Algorithmus 72 Satz 9.3 Obiger Algorithmus kann in Zeit O (n4 · t) mit Fehlerwahrscheinlichkeit kleiner e−t einen MinCut berechnen. Bemerkung 9.4 Varianten dieses Algorithmus laufen in Zeit O (t · n2 log(n)2 ) und sind damit schneller als alle auf Flußalgorithmen basierenden deterministischen Verfahren! 14. Oktober 2008, Version 0.6