Effiziente Algorithmen - Heinz Nixdorf Institut

Werbung
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
Herunterladen