Dynamische Programmierung Julian Brost 11. Juni 2013 Julian Brost Dynamische Programmierung 11. Juni 2013 1 / 39
Gliederung 1 Was ist dynamische Programmierung? Top-Down-DP Bottom-Up-DP 2 Matrix-Kettenmultiplikation 3 Longest Common Subsequence 4 Longest Increasing Subsequence 5 Zusammenfassung Julian Brost Dynamische Programmierung 11. Juni 2013 2 / 39
Fibonacci-Folge F 0 = 0 F 1 = 1 F n = F n 1 + F n 2 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,... Julian Brost Dynamische Programmierung 11. Juni 2013 3 / 39
Fibonacci-Folge F 6 F 4 F 5 F 2 F 3 F 3 F 4 F 0 F 1 F 1 F 2 F 1 F 2...... F 0 F 1 F 0 F 1 Julian Brost Dynamische Programmierung 11. Juni 2013 4 / 39
Fibonacci (naiv) O(2 n ) long long fib_naiv(int n) { if (n == 0) return 0; if (n == 1) return 1; return fib_naiv(n-1) + fib_naiv(n-2); Julian Brost Dynamische Programmierung 11. Juni 2013 5 / 39
Das geht besser! Dynamische Programmierung: Verfahren zur Lösung von (Optimierungs-)Problemen Berechnung der Gesamtlösung unter effizienter Verwendung überlappender Teillösungen Eingeführt durch Richard Bellman Julian Brost Dynamische Programmierung 11. Juni 2013 6 / 39
Top-Down-DP Idee: Rekursive Implementierung merkt sich bereits berechnete Teilergebnisse in einem Array (Memoization) Bei Aufruf der Funktion: Überprüfen, ob der Wert bereits berechnet wurde Falls ja: Keine erneute Berechnung sondern direktes Zurückgeben des bekannten Werts Ansonsten: Berechnen, Speichern und Zurückgeben des Werts Julian Brost Dynamische Programmierung 11. Juni 2013 7 / 39
Fibonacci (Top-Down) O(n) #define UNKNOWN 0 long long table[max_n] = {; // initialisiert // auf UNKNOWN long long fib_top_down(int n) { if (n == 0) return 0; if (n == 1) return 1; if (table[n] == UNKNOWN) // noch nicht berechnet table[n] = fib_top_down(n-1) + fib_top_down(n-2); return table[n]; Julian Brost Dynamische Programmierung 11. Juni 2013 8 / 39
Bottom-Up-DP Idee: Teilprobleme sortieren und iterativ lösen Bei jedem Schritt: Lösen des aktuellen Problems durch Zugriff auf bereits berechnete Teillösungen (direkter Arrayzugriff statt rekursiver Aufruf) Julian Brost Dynamische Programmierung 11. Juni 2013 9 / 39
Fibonacci (Bottom-Up) O(n) long long fib_bottom_up(int n) { long long table[n+2]; table[0] = 0; table[1] = 1; for (int i = 2; i <= n; i++) { table[i] = table[i-1] + table[i-2]; return table[n]; Julian Brost Dynamische Programmierung 11. Juni 2013 10 / 39
Fibonacci: Ausführungszeit Naiv fib naiv(50) 1m 54.688s Top-Down fib top down(50) 0.003s Bottom-Up fib bottom up(50) 0.003s Julian Brost Dynamische Programmierung 11. Juni 2013 11 / 39
Top-Down vs. Bottom-Up Top-Down Pro Einfachere Implementierung Nur tatsächlich benötigte Teillösungen werden berechnet Contra Overhead durch Rekursion Beschränkte Rekursionstiefe beim ICPC (Stacklimit) Julian Brost Dynamische Programmierung 11. Juni 2013 12 / 39
Top-Down vs. Bottom-Up Bottom-Up Pro Kein Overhead durch Rekursion Gleiche asymptotische Laufzeit, aber geringerer konstanter Faktor Contra Komplizierter zu Implementieren Evtl. Berechnung nicht benötigter Teillösungen Julian Brost Dynamische Programmierung 11. Juni 2013 13 / 39
Matrix-Kettenmultiplikation Julian Brost Dynamische Programmierung 11. Juni 2013 14 / 39
Matrix-Kettenmultiplikation Matrizen A 1, A 2, A 3,..., A n gegeben Wir wollen A 1 A 2 A 3... A n möglichst effizient, d.h. mit möglichst wenigen skalaren Multiplikationen berechnen Matrizenmultiplikation ist assoziativ, Optimierung über Klammerung möglich Julian Brost Dynamische Programmierung 11. Juni 2013 15 / 39
Matrix-Kettenmultiplikation Wir definieren die Kosten für die Multiplikation zweier Matrizen A und B als Anzahl der notwendigen Skalaren Multiplikationen: cost(a, B) = A.rows A.cols B.cols Julian Brost Dynamische Programmierung 11. Juni 2013 16 / 39
Matrix-Kettenmultiplikation Ein Beispiel: A R 10 100 B R 100 5 C R 5 50 ((AB)C) cost(a, B) + cost(ab, C) = 7500 (A(BC)) cost(b, C) + cost(a, BC) = 75000 Julian Brost Dynamische Programmierung 11. Juni 2013 17 / 39
Matrix-Kettenmultiplikation A i..j := A i A i+1... A j, i j Falls i = j: trivial, keine Multiplikationen notwendig, also keine Kosten Sonst: Wir teilen das Produkt an einer Stelle k (i k < j): A i..k = A i..k A k+1..j Kosten ergeben sich durch die Kosten für die Berechnung von A i..k, A k+1..j sowie den Kosten für die Multiplikation dieser Beiden Matrizen Wir suchen in jedem Schritt ein k, sodass diese Kosten minimal werden Julian Brost Dynamische Programmierung 11. Juni 2013 18 / 39
Matrix-Kettenmultiplikation Sei m[i, j] die Anzahl der notwendigen skalaren Multiplikationen Wir suchen dann (insgesamt) m[1, n] { 0 falls i = j m[i, j] = min i..k, A k+1..j ) i k<j falls i < j Julian Brost Dynamische Programmierung 11. Juni 2013 19 / 39
Matrix-Kettenmultiplikation O(N 3 ) Matrix matrices[max_n] = { /*... */ ; int m[max_n][max_n]; int matrix_chain(int N) { for (int i = 0; i < N; i++) { m[i][i] = 0; for (int l = 2; l <= N; l++) { for (int i = 0; i <= N - l; i++) { int j = i + l - 1; m[i][j] = INT_MAX; for (int k = i; k < j; k++) { int q = m[i][k] + m[k+1][j] + matrices[i].rows * matrices[k].cols * matrices[j].cols; if (q < m[i][j]) { m[i][j] = q; return m[0][n-1]; Julian Brost Dynamische Programmierung 11. Juni 2013 20 / 39
Matrix-Kettenmultiplikation Um eine optimale Klammerung zu rekonstruieren: Zusätzliches Array s[i, j] speichert jeweils den Parameter k, für den die Teilung von A i..j optimal war. Mit diesen Informationen kann eine Funktion dann rekursiv jeweils eine optimale Klammerung für A i..k und A k+1..j ausgeben. Julian Brost Dynamische Programmierung 11. Juni 2013 21 / 39
Matrix-Kettenmultiplikation int s[max_n][max_n]; // (+) int matrix_chain(int N) { for (int i = 0; i < N; i++) { m[i][i] = 0; for (int l = 2; l <= N; l++) { for (int i = 0; i <= N - l; i++) { int j = i + l - 1; m[i][j] = INT_MAX; for (int k = i; k < j; k++) { int q = m[i][k] + m[k+1][j] + matrices[i].rows * matrices[k].cols * matrices[j].cols; if (q < m[i][j]) { m[i][j] = q; s[i][j] = k; // (+) return m[0][n-1]; Julian Brost Dynamische Programmierung 11. Juni 2013 22 / 39
Matrix-Kettenmultiplikation void print_optimal_parens(int i, int j) { if (i == j) { cout << " " << i << " "; else { cout << "("; int k = s[i][j]; print_optimal_parens(i, k); print_optimal_parens(k+1, j); cout << ")"; Aufruf: print optimal parens(0, N-1); Julian Brost Dynamische Programmierung 11. Juni 2013 23 / 39
Longest Common Subsequence Julian Brost Dynamische Programmierung 11. Juni 2013 24 / 39
Longest Common Subsequence Seien X = {x 1, x 2,..., x m und Y = {y 1, y 2,..., y n Folgen, sowie Z = {z 1, z 2,..., z k eine LCS von X und Y. 1 Wenn x m = y n, dann z k = x m = y n und Z k 1 ist LCS von X m 1 und Y n 1. 2 Wenn x m y n, dann impliziert z k x m, dass Z eine LCS von X m 1 und Y ist. 3 Wenn x m y n, dann impliziert z k y n, dass Z eine LCS von X und Y n 1 ist. Julian Brost Dynamische Programmierung 11. Juni 2013 25 / 39
Longest Common Subsequence Sei c[i, j] die Länge einer LCS von X i und Y j Wir suchen dann (insgesamt) c[m, n] 0 falls i = 0 oder j = 0 c[i, j] = c[i 1, j 1] + 1 falls i, j > 0 und x i = y j max(c[i, j 1], c[i 1, j]) falls i, j > 0 und x i y j Julian Brost Dynamische Programmierung 11. Juni 2013 26 / 39
LCS O(MN) string X = /*... */, Y = /*... */ ; int lcs[max_m+1][max_n+1]; int LCS() { int m = X.size(); int n = Y.size(); for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (X[i-1] == Y[j-1]) { lcs[i][j] = lcs[i-1][j-1] + 1; else { lcs[i][j] = max(lcs[i-1][j], lcs[i][j-1]); return lcs[m][n]; Julian Brost Dynamische Programmierung 11. Juni 2013 27 / 39
Longest Common Subsequence Um eine LCS zu rekonstruieren: Keine Speicherung von weiteren Informationen notwendig, da sich die Entscheidung in O(1) aus X[i-1], Y[j-1], lcs[i-1][j] und lcs[i][j-1] rekonstruieren lässt (alternativ aber trotzdem möglich). Julian Brost Dynamische Programmierung 11. Juni 2013 28 / 39
Longest Common Subsequence void print_lcs(int i, int j) { if (i == 0 j == 0) { return; if (X[i-1] == Y[j-1]) { print_lcs(i-1, j-1); cout << X[i-1]; else if (lcs[i-1][j] >= lcs[i][j-1]) { print_lcs(i-1, j); else { print_lcs(i, j-1); Aufruf: print LCS(m, n); 1 1 m = X.size(), n = Y.size() Julian Brost Dynamische Programmierung 11. Juni 2013 29 / 39
Longest Increasing Subsequence Julian Brost Dynamische Programmierung 11. Juni 2013 30 / 39
Longest Increasing Subsequence Vorgehensweise: Wir fügen alle Folgenglieder der Reihe nach sortiert in ein Array (hier: minval) ein, d.h. 1 wir suchen den ersten Wert im Array, der größer oder gleich dem aktuellen Folgenglied ist und ersetzen ihn durch dieses 2 falls kein solcher Wert im Array existiert (d.h. alle Werte kleiner sind), fügen wir den Wert am Ende an Im Fall (2) haben wir eine längere LIS gefunden und erhöhen die Variable maxlen Julian Brost Dynamische Programmierung 11. Juni 2013 31 / 39
LIS O(N log N) int seq[max_n] = { /*... */ ; // Zu betrachtende Folge int minval[max_n]; int LIS(int N) { int maxlen = 0; for (int i = 0; i < N; i++) { int lis = lower_bound(minval, minval + maxlen, seq[i]) - minval + 1; if (lis > maxlen) { maxlen = lis; minval[lis-1] = seq[i]; else if (seq[i] < minval[lis-1]) { minval[lis-1] = seq[i]; return maxlen; Julian Brost Dynamische Programmierung 11. Juni 2013 32 / 39
Longest Increasing Subsequence Um eine LIS zu rekonstruieren: Speichern des Index in minval statt des Werts Zusätzliches Array, um jeweils den Vorgänger zu speichern Dieses lässt sich dann von hinten durchgehen, um eine LIS zu rekonstruieren Julian Brost Dynamische Programmierung 11. Juni 2013 33 / 39
Longest Increasing Subsequence int previd[maxn]; int maxid = -1; int LIS(int N) { int maxlen = 0; for (int i = 0; i < N; i++) { int lis = lower_bound(minval, minval + maxlen, i, // (*) [](int x, int y){ return seq[x] < seq[y]; ) - minval + 1; // (*) if (lis == 1) previd[i] = -1; // (+) else previd[i] = minval[lis-2]; // (+) if (lis > maxlen) { maxlen = lis; minval[lis-1] = i; maxid = i; // (+) else if (seq[i] < seq[minval[lis-1]]) { minval[lis-1] = i; return maxlen; Julian Brost Dynamische Programmierung 11. Juni 2013 34 / 39
Longest Increasing Subsequence void print_lis(int i) { if (i < 0) return; print_lis(previd[i]); cout << seq[i] << " "; Aufruf: print LIS(maxid); Julian Brost Dynamische Programmierung 11. Juni 2013 35 / 39
Zusammenfassung Julian Brost Dynamische Programmierung 11. Juni 2013 36 / 39
Wann verwende ich DP? Optimierungsprobleme mit Optimaler Substruktur Das Problem lässt sich in kleinere Teilprobleme zerlegen Eine optimale Gesamtlösung lässt sich aus optimalen Teillösungen zusammensetzen Überlappende Teilprobleme Ein einfacher rekursiver Algorithmus würde die gleichen Teilprobleme immer wieder lösen Durch Speichern der Teilergebnisse wird die Laufzeit massiv verbessert Julian Brost Dynamische Programmierung 11. Juni 2013 37 / 39
Wie verwende ich DP? 1 Überprüfen, ob DP anwendbar ist (siehe vorherige Folie) 2 Rekursive Lösung finden 3 Umsetzung mit DP (bottom-up oder top-down) 4 Rekonstruieren einer Lösung (falls erforderlich) Julian Brost Dynamische Programmierung 11. Juni 2013 38 / 39
Quellen Thomas H. Cormen, et al.: Introductions to Algorithms, Third Edition, MIT Press Tobias Werth: Dynamische Programmierung (Hallo Welt, 2004) Ludwig Höcker: Dynamische Programmierung (Hallo Welt, 2012) http://www.algorithmist.com/index.php/ Longest_Increasing_Subsequence http://en.wikipedia.org/wiki/longest_ increasing_subsequence Hallo Welt Wiki Julian Brost Dynamische Programmierung 11. Juni 2013 39 / 39