Disclaimer: Diese Mitschrift ist als Ergänzung zur Vorlesung gedacht. Wir erheben keinen Anspruch auf Vollständigkeit und Korrektheit. Wir sind jederzeit froh um Hinweise zu Fehlern oder Unklarheiten an grafdan@ethz.ch.
Wenn Sie an dieser Mitschrift mitarbeiten möchten, melden Sie sich hier.
Wir haben in der ersten Lektion gesehen, wie wir Algorithmen entwerfen und analysieren wollen. Wir haben ein Analysemodell gesehen, das sehr grob ist: Einen Algorithmus, der
Wir wollen heute nochmals Algorithmen entwerfen, die "gut" sind. Wie misst man denn die Qualität von Algorithmen?
Betrachten wir folgende
5 7 12 -48 9 36 -17 22 11 -49 49 -49 111 -117
Aus diesen Zahlen wollen wir ein Teilstück, ein Intervall, extrahieren, sodass die Summe darin maximal wird. Für so kleine Eingaben wie das Beispiel oben können wir problemlos alle Teilintervalle ausprobieren, doch für
Wir können das auch als Aktienkurs anschauen. Dann seien die Zahlen die Tagesänderungen und wir fragen uns im Nachhinein: Wann hätten wir am besten kaufen und wieder verkaufen sollen, um unseren Profit zu maximieren?
for i = 1 to n do
for j = i to n do
S := 0
for k = i to j do
S := S + d_k
merke maximales S
Wir betrachten also jedes mögliche Intervall
Maximale Laufzeit: Zählen wir die Anzahl Additionen sehr grosszügig: Jede Schleife wird maximal
Doch ist dieser Algorithmus wirklich so langsam oder haben wir einfach viel zu grob abgeschätzt? Schätzen wir die Laufzeit mal noch konservativ ab, zählen also eher zu wenig Additionen. Nehmen wir dazu nun an, dass sich der Anfang des Intervalls
Da wir die selbe, kubische untere und obere Schranke gezeigt haben, können wir also sagen, dass die Laufzeit unseres Algorithmus in
Was machen wir zuviel hier? Wir summieren immer wieder die selben Dinge auf. Können wir die Summe der Zahlen von i bis j nicht schneller bestimmen?
Es genügt, lediglich alle Präfixsummen zu berechnen: Wir berechnen für jede Position
Wir können uns nun die innerste Schleife sparen:
// Berechne die Präfixsummen in O(n)
S[0] := 0
for i := 1 to n do
S[i] := S[i-1]+d[i]
// Bilde die Summe für jedes Interval in O(n^2)
for i := 1 to n do
for j := i to n do
S := S[j]-S[i-1]
merke maximales S
Wie viel schneller sind wir damit? Das Vorberechnen der Präfixsummen geht in
Sind wir jetzt zufrieden?
Um noch schneller zu werden, wollen wir nun noch einen weiteren Trick des Algorithmenentwurfs anschauen. Es ist eine mächtige Technik, die bereits die Römer kannten.
Wie gestern, versuchen wir eine induktive Lösung zu suchen. Hier werden wir nicht einfach ein Element wegnehmen (wie bei der Rektorin gestern), sondern in der Mitte teilen:
Wir teilen unsere Zahlenfolge in zwei Teile. Wo kann die Lösung, d.h. das Teilintervall mit der grössten Zahlensumme, liegen? Wir unterscheiden drei Fälle: Entweder liegt das gesuchte Intervall genau in der linken Hälfte, genau in der rechten Hälfte, oder es enthält Teile der rechten und der linken Hälfte. Genau diese drei Fälle wollen wir anschauen und für jeden Fall das maximale Intervall berechnen. Dann müssen wir nur noch aus den drei Intervallen jenes mit der grössten Summe wählen.
Eine solche Strategie, die das Problem in kleinere Portionen aufteilt und diese separat löst und wieder zusammensetzt, nennt man "Divide and Conquer". Hat uns diese Aufteilung hier was gebracht?
Die ersten beiden Fälle könne wir einfach behandeln, indem wir das die entsprechende Hälfte separat betrachten und die Lösung (rekursiv mit dem gleichen Verfahren) berechnen. Wie können wir aber die Lösung für den dritten Fall berechnen?
Wir suchen links das beste Teilstück, das am rechten Rand endet, und setzen es zusammen mit dem besten Teilstück im rechten Teil, das am linken Rand endet.
Wir müssen also die Präfixsummen der rechten und die Suffixsummen der linken Hälfte betrachten. Wir haben bereits gesehen, dass diese in linearer Zeit berechnet werden können. Wir addieren also die maximale Präfixsumme der rechten Hälfte und die maximale Suffixsumme der linken Hälfte. So können wir die Lösung für den dritten Fall in linearer Zeit ausrechnen.
Analyse:
Nun wollen wir die Laufzeit unseres Algorithmus analysieren, um zu sehen, ob wir überhaupt eine Verbesserung erzielen konnten.
Wir schreiben
Dabei sei
Wie löst man eine solche Rekursionsgleichung? Wir machen, was wir bis jetzt immer gemacht haben. Wir setzten ein paar Mal ein und schauen, ob wir ein Gefühl für die Lösung kriegen.
Das hier endet falls
Also vermuten wir sowas wie
Doch stimmt das auch? Um es zu beweisen wollen wir nie die
Beweis mittels vollständiger Induktion:
zu beweisen: Es gibt
Induktionsanfang:
Induktionshyptohese (IH): Für
Induktionsschritt:
Können wir die letzte Ungleichung erfüllen? Wir haben noch etwas Spielraum, da wir
Wähle dazu z.B.
Damit wissen wir, dass unser Divide-and-Conquer Algorithmus in Zeit
Geht es noch besser?
Nochmal von vorne: Versuchen wir nochmals eine Induktion von links nach rechts.
Als Invariante nehmen wir an, dass wir nach dem
Als Invariante nehmen wir an, dass wenn wir bis Position
randmax := 0
max := 0
for i = 1 to n do
randmax := randmax + d[i]
if randmax < 0 then randmax := 0
if max < randmax then max := randmax
Dieses Verfahren hat nun gar nur lineare Laufzeit! Also
Nun werden wir ehrgeizig und wollen es noch besser machen. Schaffen wir
Folgende Überlegung zeigt, dass wir nicht besser als linear sein können: Angenommen wir haben einen Algorithmus, der nicht jedes Element anschauen muss. Dann ist er nicht korrekt:
Demnach muss jeder korrekte Algorithmus alle Elemente mindestens einmal betrachten. Die Laufzeit liegt also in
Die Grösse des Inputs ist nicht immer eine untere Schranke für die Laufzeit. Erinnern wir uns an das Rektorinnen-Problem von gestern. Da war die Eingabegrösse quadratisch (jeder mit jedem), aber wir mussten nur linear viele Fragen stellen.
Fragen:
Aufgabe fürs Wochenende: Wie viel Zusatzspeicher benötigt der Divide-and-Conquer-Algorithmus?[1]