Dynamische Programmierung Hannes Schwarz - WS-06/07 Hannes.Schwarz@uni-konstanz.de Getting Ready for the ACM Programming Contest
Übersicht Übersicht Was ist dynamische Programmierung? Entwicklung eines Algorithmus Beispiel: Fibonacci-Folge Implementierung Memoization & Teilprobleme
Übersicht Rucksackproblem Implementierung Top-down vs. Bottom-up Binomialkoeffizienten Fazit Quellen
Was ist dynamische Programmierung? Definition: Algorithmen der dynamischen Programmierung kommen bei Optimierungsproblemen zur Anwendung, bei denen das Endergebnis aus einer unbekannten Kombination von vielen, nicht notwendigerweise unabhängigen Teilergebnissen berechnet werden kann. Quelle: http: //dynamische_programmierung.know-library.net
Was ist dynamische Programmierung? Einsatzgebiet: Optimierungsprobleme Endergebnis ist Kombination aus Teilergebnissen Teilergebnisse nicht zwingend unabhängig Kombination unbekannt
Woher kommt dynamische Programmierung? Teile & Herrsche ist Grundlage Geschickt implementierte Rekursion Zwischenergebnisse in Tabelle speichern Typischer Einsatz: Suche nach minimaler oder maximaler Kombination
Woher kommt dynamische Programmierung? Begriff in den 1940er Jahren von Richard Bellman geprägt Mathematisches Verfahren Programmierung bezieht sich auf das Ausfüllen der Tabelle
Entwicklung eines Algorithmus mit dynamischer Programmierung Voraussetzung: Die optimale Lösung setzt sich aus optimalen Teillösungen zusammen. 1. Charakterisierung der Struktur 2. Rekursive Definition des Wertes einer optimalen Lösung 3. Berechnung des Wertes einer optimalen Lösung (rekursiv) 4. Konstruktion der optimalen Lösung aus berechneter Information
Fibonacci-Folge Definition: f(1) = f(2) = 1 f(n) = f(n 2) + f(n 1) n > 2 Rekursiv definiert rekursive Implementierung?
Fibonacci-Folge - rekursive Implementierung static int knownf[] = new int[1000]; int fib(int n) { if( knownf[n]!= 0) { return knownf[n];} if( n == 1 n == 2) { return 1; } int newfib = fib(n-2) + fib(n-1); return newfib; }
Fibonacci-Folge - rekursive Implementierung Rekursionsbaum für f(7) exponentieller Aufwand O(2 n )
Fibonacci-Folge - rekursive Implementierung mit Köpfchen static int knownf[] = new int[1000]; int fib(int n) { if( knownf[n]!= 0) { return knownf[n];} if( n == 1 n == 2) { return 1; } int newfib = fib(n-2) + fib(n-1); return newfib; }
Fibonacci-Folge - rekursive Implementierung mit Köpfchen static int knownf[] = new int[1000]; int fib(int n) { if( knownf[n]!= 0) { return knownf[n];} if( n < 2) { return 1; } int newfib = fib(n-2) + fib(n-1); return newfib; }
Fibonacci-Folge - rekursive Implementierung mit Köpfchen static int knownf[] = new int[1000]; int fib(int n) { if( knownf[n]!= 0) { return knownf[n];} if( n < 2) { return 1; } int newfib = fib(n-2) + fib(n-1); return knownf[n] = newfib; }
Fibonacci-Folge - rekursive Implementierung mit Köpfchen Weiterhin Rekursion Rekursionsbaum für f(7) Ergebnisse werden gespeichert Zuerst lookup in Tabelle linearer Aufwand O(n)
Fibonacci-Folge Laufzeiten für fib(50) = 12586269025 Algorithmus Zeit rekursiv 13 min. 12.sek. iterativ 0.004 sek. rek. + Köpfchen 0.004 sek.
Optimale Teilprobleme Zerlegung wie bei Teile&Herrsche ABER: Teilprobleme müssen nicht unabhängig sein Zerlegung liefert Rekursionsgleichung für das Problem Teilprobleme sind optimal Basisfälle aufstellen (n=1)
Überlappende Teilprobleme Rekursion löst immer gleiche Teilprobleme überlappende Teilprobleme Teile&Herrsche : unterschiedliche Teilprobleme Einsatz von Memoization Dynamische Programmierung ist nichts weiter als das Lösen von Teilproblemen, wobei die Mehrfachberechnung von Teilproblemen aufgrund von Überschneidungen durch Memoization erschlagen wird.
Memoization Idee: ineffiziente Rekursion wird durch Füllen der Tabelle effizient Tabelle ein- oder mehrdimensionales Array Programm sieht wie folgt aus: Wert schon berechnet? zurückliefern Wert noch nicht berechnet? berechnen, speichern, zurückliefern
Rucksackproblem? 17 kg
Rucksackproblem Rucksack der Größe c N Typen von Gegenständen Jeder Typ n hat einen Wert vn sowie einen Platzbedarf von sn Jetzt packen wird den Rucksack: Die Kapazität c nicht überschritten wird Der Gesamtwert maximal ist
Rucksackproblem - Entwurf der optimalen Lösung Liste von k Gegenstandstypen n: L c = (n 1, n 2,..., n k ) Maximaler Wert des Rucksack ist die Summe aller Werte seiner Gegenstände w c = k i=1 v i
Rucksackproblem - Aufstellen der Rekursionsgleichung Lc ist für die Kapazität c eine optimale Lösung Wenn wir ein Element aus Lc entfernen ist die Restliste eine optimale Lösung für die Kapazität c - snk Bei jedem Schritt ist eine Auswahl zu treffen: füge weiteres Element hinzu, so dass Die Kapazität nach wie vor unter der Gesamtkapazität liegt Der erhöhte Gesamtwert maximal ist
Rucksackproblem - rekursive Lösung Geg.: static class Item{int size; int val;} Item items[]; static int knap(int cap) { int i, space, max, t; for(i=0, max=0; i<items.length;i++) if((space = cap-items[i].size) >=0) if((t=knap(space) + items[i].val)>max) max=t; return max; }
Rucksackproblem - rekursive Lösung Geg.: static class Item{int size; int val;} Item items[]; FINGER WEG!!!! static int knap(int cap) { int i, space, max, t; for(i=0, max=0; i<items.length;i++) if((space = cap-items[i].size) >=0) if((t=knap(space) + items[i].val)>max) max=t; return max; }
Rucksackproblem - rekursive Lösung mit Memoization static int knap(int cap) { int i, space, max, t; if(maxknown[cap]!= unknown) return maxknown[cap]; for(i=0, max=0; i<items.length;i++) if((space = cap-items[i].size) >=0) if((t=knap(space) + items[i].cal)>max) max=t; return max; }
Rucksackproblem - rekursive Lösung mit Memoization static int knap(int cap) { int i, space, max, t; if(maxknown[cap]!= unknown) return manknown[cap]; for(i=0, max=0; i<items.length;i++) if((space = cap-items[i].size) >=0) if((t=knap(space) + items[i].val)>max) max=t; maxknown[cap] = max; return max; }
Rucksackproblem - rekursive Lösung mit Memoization Laufzeit ohne Memoziation c Zeit 100 0.9sec 125 51sec 135 4min 28sec 150 52min 48sec Laufzeit mit Memoziation c Zeit 1.000 5ms 100.000 20ms 500.000 80ms 1.000.000 145ms von exponentieller zu linearer Laufzeit
Rucksackproblem - Rekonstruktion des Lösungswegs Bisheriges Ergebnis: Wert unseres maximal gepackten Rucksacks Welche Gegenstände sind eigentlich im Rucksack?
Rucksackproblem - Rekonstruktion des Lösungswegs In jedem Schritt Gegenstand merken - weiterer Array Für jeden Kapazitätswert einen eindeutigen Gegenstand Dieser Gegenstand ist für die Größe optimal und daher fest
Rucksackproblem - Rekonstruktion des Lösungswegs Rekonstruktion beginnt bei der Gesamtkapazität c Der Index des ersten Gegenstands befindet sich in itemknown[c] Wurde der Gegenstand aufgenommen c = c -itemknown[c].size Nächster Index also itemknown[c -itemknown[c].size] usw.
Rucksackproblem - die endgültige Lösung static int knap(int cap) { int i, space, max, maxi, t; if(maxknown[cap]!= unknown) return manknown[cap]; for(i=0, max=0; i<n;i++) if((space = cap-items[i].size) >=0) if((t=knap(space) + items[i].val)>max) max=t; maxi = i; maxknown[cap] = max; itemknown[cap] = items[maxi]; return max; }
Top-down Rekursive Berechnung ist ein Top-down -Ansatz Bisher Effizienzgewinn durch Memoization Probleme durch: Hohe Rekursionstiefen (Stack Overflow) Overhead durch Funktionsaufrufe
Bottom-up Zuerst Tabelle mit Teillösungen füllen Lösung schliesslich aus Tabelle auslesen Performanter als Top-down -Ansatz Bottom-up -Ansatz schwerer zu finden
Bottom-up vs. Top-down Bottom-up -Ansatz Zuerst wird eine Optimallösung von Teilproblemen bestimmt Danach Auswahl der optimalen Teillösungen zu Konstruktion einer Lösung des ursprünglichen Problems Top-down -Ansatz Zuerst Auswahl einer vielversprechenden Alternative Danach Lösung der resultierenden Teilprobleme
Binomialkoeffizienten Problem: Anzahl Möglichkeiten k (numerierte) Studenten aus einem Seminarraum mit n Personen zu wählen und zum World Final zu schicken Lösung: ( ) n k = n! (n k)!k!
Binomialkoeffizienten Rekursionsgleichung ( ) n k = ( ) n 1 k 1 + ( ) n 1 k Das Pascalsche Dreieck n = 0 1 n = 1 1 1 n = 2 1 2 1 n = 3 1 3 3 1 n = 4 1 4 6 4 1 n = 5 1 5 10 10 5 1 n = 6 1 6 15 20 15 6 1
Binomialkoeffizienten - Die Bottom-up -Lösung Zur Lösung Pascalsche Dreieck Array[n][n+1] Die Ränder [0] [0] bis [0] [n] und [0] [0] bis [n] [n] werden mit Einsen gefüllt Zur Berechnung wird auf vorherige Ergebnisse zurückgegriffen An Position [n] [m] steht der gesuchte Wert
Binomialkoeffizienten - Die Bottom-up -Lösung static void bin() { int dreieck [][] = new int [7][]; for ( int i = 0; i < dreieck.length; i++ ) { dreieck[i] = new int[i+1]; for ( int j = 0; j <= i; j++ ) { if ( (j == 0) (j == i) ) dreieck[i][j] = 1; else dreieck[i][j]=dreieck[i-1][j-1]+dreieck[i-1][j]; System.out.print( dreieck [i][j] + " " ); } System.out.println(); } }
Binomialkoeffizienten - Die Bottom-up -Lösung Kein Overhead durch Funtionsaufrufe Aber: zuviele Werte berechnet Für den Wert xi,j sind xi-1,j-1 und xi-1,j Werte relevant Zur Berechnung von benötigt ( ) n m werden also keine xi,j mit j > m Ausfüllen der Tabelle in jeder Zeile zum m-ten Wert ausreichend
Fibonacci-Folge - rekursive Implementierung mit Memoization static int knownf[] = new int[1000]; int fib(int n) { if( knownf[n]!= 0) { return knownf[n];} if( n < 2) { return 1; } int newfib = fib(n-2) + fib(n-1); return knownf[n] = newfib; }
Fibonacci-Folge - iterative Implementierung (Bottom-up) static int iterativ(int n) { int[] ita = new int[n+1]; ita[1] = 1; ita[2] = 1; for(int i = 3; i < ita.length; i++ ) { ita[i] = ita[i-2] + ita [i-1]; } return ita[n]; }
Fazit Suche nach Optimum Optimales Ergebnis lässt sich in optimale Teilergebnisse aufteilen Jeder Schritt zum Ziel benötigt eine Auswahl Überlappungen können auftreten
Fazit Nicht immer möglich die Lösung kleinerer Probleme so zu kombinieren, dass das große Problem gelöst ist Teilprobleme können unvertretbar groß sein Nicht genau angegeben welche Probleme DP effizient löst
Weitere Einsatzgebiete von dynamischer Programmierung Bioinformatik Sequenzierung von Genen und Proteinen Ähnliche Problemstellungen wie bei Stringvergleichen Linguistik CYK -Algorithmus
Literaturempfehlungen R. Sedgewick; Algorithmen in Java; Pearson, 2003 S. Skiena; The Algorithm Design Manual; Springer, 1998 V. Heun; Grundlegende Alhorithmen; Vieweg, 2003
Internetquellen http://de.wikipedia.org/wiki/dynamische_programmierung http://www.informatik.uni-leipzig.de/lehre/heyer0001/ad2-vorl2/index.htm http://ddi.uni-muenster.de/personen/marco/dynprogrammieren.pdf http://www2.informatik.uni-erlangen.de/lehre/ss2006/hallowelt/ dynamische_programmierung.pdf?language=en http://www2.informatik.uni-erlangen.de/lehre/ss2005/hallowelt/ dyn_programming.beamer.pdf?language=de
Vielen Dank für die Aufmerksamkeit!