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.
Heute wollen wir, nach den vielen spezifischen Algorithmen und Datenstrukturen der letzten Wochen, zu den allgemeinen Entwurfstechniken zurückkehren.
Wir kehren zurück zum allerersten Entwurfsprinzip: die Induktion. Wie können wir es auch für schwierigere Beispiele anwenden?
Diese rekursiv definierte Zahlenfolge ist uns allen bekannt:
Wie implementieren wir eine solche Folge, also eine Funktion, die uns das
Implementieren wir es mit Rekursion, geht das ganz leicht:
fib(n) {
if(n<2) return n;
return fib(n-1) + fib(n-2);
}
Führen wir dieses Programm für
In der Implementierung oben berechnen wir die selbe Fibonacci-Zahl mehrmals. Im oben dargestellten Baum wird beispielsweise fib(98) und fib(97) mehrfach ausgerufen. Je größer dieser Baum wird, desto öfter berechnen wir ein und denselben Wert. Es ist leicht zu sehen, dass dieser Aufrufbaum exponentielle Grösse hat (think about it!).
Eleganter können wir eine Fibonacci-Zahl iterativ berechnen:
Fib(0) = 0;
Fib(1) = 1;
for(int i=0; i>n-2; i++) {
Fib(i+2) = Fib(i+1) + Fib(i);
}
Dieser Trick, der vermeidet, dass wir das selbe
Nun zu einem interessanteren Beispiel solcher Induktionen.
Wir haben eine Leiterplatte mit zwei Reihen von
Nummerieren wir oben von
Wie finden wir in einer Permutation der Zahlen
Wenn wir sämtliche
Wir betrachten die ersten
Wir müssen uns allerdings nicht alle Teilfolgen gleicher Länge merken. Es reicht, wenn wir uns diejenige Lösung merken, deren End-Zahl am kleinsten ist. Können wir irgendeine andere Teilfolge derselben Länge erweitern, so können wir auch die Teilfolge mit kleinster End-Zahl erweitern.
Es ist aber nicht notwendigerweise so, dass wir immer die längste gefundene aufsteigende Teilfolge erweitern können. Deshalb müssen wir uns für jede Folgenlänge die kleinste End-Zahl einer aufsteigenden Folge dieser Länge merken.
Invariante: Für jede schon gesehene Folgenlänge
Beispiel
3 2 5 1 6 4
Initilisiere alles mit unendlich (hier '-')
Teillänge: 1 2 3 4 5
Endzahl: - - - - -
Betrachte die '3': Erste Sequenz der Länge 1
Teillänge: 1 2 3 4 5
Endzahl: 3 - - - -
Betrachte die '2': Bessere Sequenz der Länge 1
Teillänge: 1 2 3 4 5
Endzahl: 2 - - - -
Betrachte die '5': Erste Sequenz der Länge 2
Teillänge: 1 2 3 4 5
Endzahl: 2 5 - - -
Betrachte die '1': Bessere Sequenz der Länge 1
Teillänge: 1 2 3 4 5
Endzahl: 1 5 - - -
Betrachte die '6': Erste Sequenz der Länge 3
Teillänge: 1 2 3 4 5
Endzahl: 1 5 6 - -
Betrachte die '4': Bessere Sequenz der Länge 2
Teillänge: 1 2 3 4 5
Endzahl: 1 4 6 - -
Aus dieser Tabelle können wir nun direkt ablesen, dass
Elemente: 3 2 5 1 6 4
Vorgänger: - - 2 - 5 1
Beim Ausfüllen der Tabelle suchen wir in jedem Schritt nach dem kleinsten Eintrag, der grösser ist als unser neues Element, und schreiben dort unser neues Element hin. Die Position dieses Eintrages können wir mittels binärer Suche finden, weil die Tabelle zu jedem Zeitpunkt aufsteigend sortiert ist (think about it!). Somit dauert ein Eintrag nur
Wenn wir etwas Zusätzliches über die Lösungslänge wissen, dann geht es oft gar noch schneller. Wissen wir beispielsweise, dass die längste Teilfolge fast Länge
Wieviele Veränderungen braucht es um von einem Wort auf ein anderes zu kommen?
LIEBE (Zeichen entfernen)
LEBE (Zeichen hinzufügen)
LEBER (Zeichen ändern)
LEDER
Wir dürfen die drei Operationen hinzufügen, entfernen, ändern in beliebiger Folge ausführen. Was ist die kürzeste Folge von Editieroperationen, die von LIEBE
nach LEDER
führt? Die Länge einer solchen kürzesten Folge heisst Editierdistanz.
Die folgende kompakte Darstellung dieser Operationen nennt man Alignment:
LIEBE_
L_EDER
Hier wird in der zweiten Position im unteren Wort eine Lücke eingefügt und in der vierten Position ein Zeichen ersetzt. Aus so einem Alignment, lässt sich die Anzahl Operationen direkt ablesen.
An jeder Position können vier mögliche Fälle auftreten:
_ X X X
X _ X Y
Es kann entweder oben oder unten eine Lücke eingefügt werden, oder es steht sowohl oben als auch unten ein Zeichen. Es kann dasselbe Zeichen oder unterschiedliche Zeichen oben und unten stehen. Der Fall, bei den oben und unten eine Lücke steht, muss nicht berücksichtigt werden, da dies unsinnig ist. Man könnte diese Position aus dem Alignment entfernen.
Uns interessiert das Alignment, das die kleinste Anzahl von Editieroperationen repräsentiert.
Das Optimalitätsprinzip der dynamischen Programmierung besagt nun Folgendes: Wenn ich eine optimale Lösung habe, dann ist eine Teillösung dieser Lösung selbst auch eine optimale Teillösung. Wäre das nicht so, so könnten wir diese Teillösung durch eine bessere ersetzen, wodurch auch eine bessere Gesamtlösung entstehen würde. Das ist aber ein Widerspruch, da wir von einer optimalen Lösung ausgegangen sind. Dieses Prinzip lässt sich auf das Sequence Alignment Problem anwenden.
Ein möglichst kurzes Alignment entspricht einer kleinen Editierdistanz. Von abc
nach edf
kann man z.B mit sechs Operationen kommen, oder auch nur mit drei:
a b c _ _ _
_ _ _ d e f
a b c
d e f
Verallgemeinert betrachten wir die Eingabe
Wir bezeichnen nun die Kosten des optimalen Alignments für die beiden Anfangsstücke bis
Wobei
Zum Einsetzen eines Eintrages betrachten wir also nur die drei Einträge oben, links und links oben (sofern überhaupt vorhanden). Machen wir es am Beispiel FISCH
vs. FROSCH
und schreiben das Tableau hin: (die ->
und \_
deuten dabei an, woher in einigen Zellen das Minimum herkam)
ED
0 1 2 3 4 5
F I S C H
0 0-> 1-> 2-> 3-> 4-> 5
|\_
1 F 1 0-> 1-> 2-> 3-> 4
\_ \_ \_
2 R 2 1 1-> 2-> 3-> 4
\_ \_ \_ \_
3 O 3 2 2 2-> 3-> 4
\_
4 S 4 3 3 2 3 4
\_
5 C 5 4 4 3 2-> 3
\_
6 H 6 5 5 4 3 2
Die Distanz zwischen FISCH
und FROSCH
ist also
FI_SCH
FROSCH
Wieviel Zeit benötigen wir nun, um diese Tabelle auszuführen? Die Tabelle hat
Das eben vorgestellte Verfahren nennt man Dynamisches Programm. Die Lösungsmethode basiert auf dem oben beschriebenen Optimalitätsprinzip und wird Dynamische Programmierung[1] genannt.
Wir wollen
Weil das Assoziativitätsgesetz bei der Matrixmultiplikation gilt, können wir die Reihenfolge der Multiplikationen unterschiedlich wählen.
Das Resultat ist zwar immer das gleiche, aber der Rechenaufwand kann sich je nach Klammerung ändern.
Für eine Multiplikation der Matrix
Wenn wir nun drei Matrizen multiplizieren sollen mit Grössen
Das Bestimmen der optimalen Reihenfolge geht hoffentlich viel schneller das anschliessende Ausführen der Multiplikationen. Für eine Bibliothek mathematischer Software kann es also durchaus Sinn machen sich vorher zu überlegen was die beste Multiplikationsreihenfolge ist.
Induktiv überlegen wir uns: Was ist die letzte Matrixmultiplikation, die wir ausführen? Die beiden Operanden dieser finalen Multiplikation hängen davon ab, welche zusammenhängenden Teilstücke der Matrixfolge wir schon miteinander multipliziert haben. Wenn wir für einen Index
Wir suchen nun das beste
M | j n
-------------------------
| |
i |_____M(i,j)
|
n |
Laufzeitanalyse: Die Tabelle hat
Als Übungsaufgabe: Wie müssen wir das Tableau erweitern, sodass wir die Lösung (die optimale Klammerung) rasch auslesen können.