Dynamisches Programmieren SS09

Aus ProgrammingWiki
< AuK
Archivierte Version vom 2009-05-04T21:24:12+00:00. Aktuelle Version

Inhaltsverzeichnis

Autoren: Markus Ullrich & Norbert Baum

Dev-Status


Eigenschaften

ToDo - Markus

Eine Sache vorweg:

Der Begriff "Programmierung" bezieht sich in diesem Kontext nicht auf das schreiben von Code , sondern vielmehr auf eine tabellarische Methode zum Lösen von Optimierungsproblemen. Demnach sollte die Methode besser "Dynamisches Optimieren" heißen, was jedoch die traditionelle Bezeichnung nicht mehr verdrängen kann.

Im folgenden werden nun zunächst die Eigenschaften dynamischer Programmierung erläutert.

Bottom-up

Genau wie die Teile-und-Herrsche-Verfahren, löst auch die Dynamische Programmierung Probleme, durch die Kombination der Lösungen von Teilproblemen. Allerdings nutzt die dynamische Programmierung einen Bottom-up-Ansatz, bei dem alle Teillösungen "von unten her" berechnet werden. Die bereits berechneten Teillösungen werden in einer Tabelle gespeichert, womit deren erneute Berechnung entfällt. Dabei wird erwartet, dass eine optimale Gesamtlösung aus optimalen Teillösungen aufgebaut werden kann. Eine solche Lösung sollte bewusst nicht als die optimale Lösung bezeichnet werden, da es verschiedene Lösungen geben kann, die den optimalen Wert besitzen.

Schwierigkeiten

Wenn nun aus der rekursiven Beschreibung eine optimale Lösung ebenfalls rekursiv erstellt werden soll, stoßen wir auf das Problem, dass wir selten unabhängige und überlappungsfreie Teilprobleme erhalten. Das wird schon deutlich wenn wir uns die Berechnung von fib(5) ansehen. Dabei müssen bei einer rekursiven Lösung erheblich viele Teilprobleme mehrfach berechnet werden. Im Bild sind diese Probleme grau hervorgehoben, ebenso wie fib(1) und fib(0), da diese bekannte Werte sind die ohnehin nicht 'berechnet' werden müssen.

Fibonacci-baum.png

Weiterhin kann es passieren das die zu lösenden Teillprobleme unausgewogen sind. Das bedeutet, dass ein besonders schwierig zu lösendes Teilproblem den Erfolg der ganzen Methode zunichte macht.

Da bei dynamischer Programmierung alle Teilprobleme berechnet werden kann eine sehr große Anzahl dieser ebenfalls zum Problem werden. So zum Beispiel beim Schach, wo es bekanntermaßen sehr viele Teilprobleme gibt, also die Kombination aller möglichen Positionen aller Figuren auf dem Brett. Zudem ist nicht bei jedem Problem garantiert, dass aus der Kombination der optimalen Teillösungen eine optimale Gesamtlösung entsteht.

Überlappenden Teilprobleme

Wie bereits bekannt finden wir selten Probleme, die aus unabhängigen Teilproblemen bestehen und somit eine rekursive Lösung einen erheblichen Mehraufwand betreiben würde. Die Anwendung Dynamischer Programmierung macht jedoch vor allem in solchen Fällen Sinn, also wenn Teilprobleme ihrerseits gemeinsame Teilprobleme nutzen. Dabei entfällt die erneute Berechnung von Teilproblemen, da deren Lösungen ja in einer Tabelle gespeichert werden. Wenn man sich das Beispiel der Berechnung von fib(5) ansieht, kann man sehr gut erkennen, dass somit die Berechnung von fib(n-2) komplett entfallen kann, da der Wert bereits durch die Berechnung von fib(n-1) bekannt ist.

Der damit eingesparte Zeitaufwand sollte dabei natürlich über dem zeitlichen Mehraufwand durch die Berechnung aller nicht benötigten Teilprobleme liegen. Das ist bei der Berechnung der Fibonacci-Zahlen natürlich immer der Fall, da sämtliche Teilprobleme zur Berechnung benötigt werden.

Iterativ oder rekursiv?

Unseren Überlegungen zufolge wäre es also nicht von Vorteil einen rekursiven Lösungsansatz zu verwenden, da eine rekursive Lösung ineffizienter ist und somit der rekursive Lösungsansatz erst in eine iterative Lösung übertragen werden muss. Dabei ist ein erheblich hoher Denkaufwand zu leisten der somit auch in hohem Maße fehleranfällig ist.

Aber gibt es eine Möglichkeit die Effizienz einer rekursiven Lösung zu steigern, und zwar: memoizing.

Dabei werden die rekursiv berechneten Teilprobleme, wie bei einem iterativen Ansatz in einer Tabelle zwischengespeichert. Vor der Berechnung eines Teilproblems wird nun zunächst in dieser Tabelle nachgeschlagen ob bereits eine Lösung vorhanden ist. Ist dies der Fall kann die erneute Berechnung entfallen. wenn nicht, so wird der gesuchte Wert berechnet und danach in die Tabelle eingetragen.

Somit können wir also doch einen rekursiven Lösungsansatz verwenden. Da dafür erheblich weniger Denkaufwand zu leisten ist, ist somit unsere Lösung weniger fehleranfällig.

Im folgenden werden nun iterative und rekursive Lösungen am Beispiel des bereits bekannten Rucksackproblems verglichen.

Iterative Berechnung

ToDo - Norbert

Bei der Iterativen Berechnung spricht man dann, wenn ein schrittweiser Zugriff/Ablauf/Berechnung zum einsatz kommt. Durch die Schrittweise Berechnung werde auch Teilberechnungen/Teilprobleme berechnet die evtl. nicht benötigt werden. Aber es kommt zu keiner Mehrfachberechnung.

Beim Rucksackproblem wird die Tabelle schrittweise ausgerechnet und befüllt, deswegen sprechen wir auch bei der ersten Lösung von der Iterativen Lösung des Rucksackproblems.


Die folgende Formel zeigt erst einmal die rekursive Beschreibung der Lösung.

$wert(i,g) = \begin{cases} 0, & wenn \ 1 =0 \\ wert(i-1, g), & wenn \ i > 0 \ und \ g-g_{i} < 0\\ max(wert(i-1,g), wert(i-1, g-g_{i})+w_{i}, & sonst \\ \end{cases}$

Die Umwandlung aus der rekursiven in eine iterative Berechnung ist nicht so trival wie man evtl. denkt. Denn da müssen die rekursiven aufrufe in zwei verschachtelte for-anweisungen verpackt werden.

Prozedur: gesamtwertIterativ
Zweck: Rucksack Algorithmus zur Berechnung des maximalen Werts
Parameter: int kapazitaet
int-Array zeileAlt
int-Array zeileNeu
FOR z := 1 TO Anzahl der gegenstaende STEP 1 DO
 Kopiere die zeileAlt auf zeileNeu
 gewicht := das Gewicht vom dem aktuellen Gegenstand(z)
 FOR s := 0 TO kapazitaet STEP 1 Do
  wenn s < gewicht dann 
   zeileNeu[s] := zeileAlt[s] //zeileNeu erhält den Wert von zeileAlt an der Postition s
  sonst
   temp := zeileAlt[s-gwicht] + Wert von gegenstaend[z]
   zeileNeu[s] := Max(temp, zeileAlt[s]) //zeileNeu bekommt den max-Wert entweder temp oder den alten
  wenn_ende
 next
next

Ergebnis: zeileNeu[kapazitaet] // letzte Spalte im Array steht der Maximale Wert

Im Abschnitt Iterative Berechnung - Implementation wurde es in Java umgesetzt.

Rückkehr des Rucksackproblems

Das Rucksackproblem wurde zwar in den verschiedenen Kapiteln schon einmal angesprochen, aber wir werden es trotzdem noch einmal kurz erklären. Es ist auch unter den Namen Knapsack problem, Transportproblem oder einfach nur Rucksack. Dabei handelt es sich um ein bekanntes Optimierungsproblem der Kombinatorik.

Der Name Transportproblem ist schon sehr gut gewählt. Das Ziel ist, n-Gegenstände einzupacken/zutransportieren, wobei jeder Gegenstand ein Gewicht/Volumen und einen Wert hat. Jedoch sollen die n-Gegenstände so kombiniert werden, dass der Wert alle eingepackten Gegenstände maximal ist, jedoch darf das Gewicht/Volumen nicht überschritten werden.

Dieser Algorithmus oder dies Optimierungsverfahren findet Anwendung in der Beladung von LKWs, Schiffen - überall dort wo man den maximalen finanziellen Gewinn erwirtschaften will.


Bei der Dynamischen Lösung des Problems sollen nun die Probleme der rekursiven Lösung entfernt werden. Wie schon erwähnt, werden Teilprobleme/Teilberechnungen evtl. doppeltdurchgeführt. Mithilfe der Dynamischen Optimierung/Programmierung tritt dies nun nicht mehr auf. Bevor ein Wert ausgerechnet wird, wird erst einmal geschaut, ob er schon vorhanden ist, wenn ja dann wird dieser genommen, wenn nein dann wird der neue Wert berechnet und dann in die Tabelle eingetragen.

Die Implementation der dynamischen Lösung mit rekursiven ansatz und memoizing ist unter Rekursive Berechnung - mit memoizing umgesetzt wurden.

Dimension der Tabelle

ausführlicher

Wie schon oben erwähnt werden die Daten in einer Tabelle gespeichert.

Am Beispiel des Rucksackproblems hat die Tabelle so viele Zeilen wie Gegenstände+1 und Spalten wie es Kapazität+1.

Implementation

Aufgrund von der eingeschränkten Umgebung, wo wir keine eigenen Klassen nehmen können und es auch keine generischen Datentypen gibt (HashMap<>). Deswegen haben wir Point genommen. Dabei ist der erste Wert das Gewicht und der zweite Werte der Wert ist.

<eval>System.out.println("Ergebnis iterativ: " + gesamtwertIterativ(120));</eval>

Ihr Browser unterstützt Canvas nicht.

Aufgrund von verschiedenen Desktopauflösungen, haben wir uns auf 1024 x egal entschieden und somit wird maximal eine Kapazität von 30 gezeichnet. <eval></eval>

       ansichtCanvas(30);

Aufwandsbetrachtung

siehe S.104 ff

Rekursive Berechnung

ToDo - Markus

ohne memoizing

<eval> System.out.println("Ergebnis rekursiv ohne memoizing: " + gesamtwertRekursiv(120)); System.out.println("Rekursive Aufrufe: " + rekursiveAufrufe); </eval>

mit memoizing

<eval> System.out.println("Ergebnis rekursiv mit memoizing: " + gesamtwertRekursivMemo(120)); System.out.println("Rekursive Aufrufe: " + rekursiveAufrufe); System.out.println("Länge der Memoliste: " + getLengthMemoListe()); System.out.println("----------------------"); System.out.println("Ergebnis rekursiv mit memoizing: " + gesamtwertRekursivMemo(120)); System.out.println("Rekursive Aufrufe: " + rekursiveAufrufe); System.out.println("Länge der Memoliste: " + getLengthMemoListe()); </eval>

Fazit

Optimale Teilstruktur

ToDo - Markus

Vorgehensweise beim Aufdecken

Raum der Teilprobleme charakterisieren

Ungewichteter einfacher Pfad

Beispiele


Fibonacci-Zahl

ToDo - Norbert
Fib-mem fertig bauen!

ohne memoizing

<eval>System.out.println(fib(15));</eval>

mit memoizing

<eval> System.out.println(fib_mem(30)); </eval>

Rundreiseproblem

ToDo - Markus

(Ablaufkoordination von Magnetbändern)

ToDo - ?

SVN Zugang für Beispiele (Java, evtl C#/F#/Scheme)

https://norbstgr.dyndns.org:8443/svn/AuK/

Username: Jederauk

Passwort: AuK#Jeder


Ihr habt nur lese Rechte!

Files

Folien, Beispiele...

MSDN F#

F# Compiler Download (auch für Mono)

Einführung F#

Literatur

  • Th. H. Cormen, Ch. E. Leiserson, R. Rivest, C. Stein: Algorithmen - Eine Einführung, 2.Auflage, Oldenbourg Wissenschaftsverlag GmbH 2007, ISBN 978-3-486-58262-8
  • Gessner, Wacker: Dynamische Optimierung - Einführung-Modelle-Computerprogramme, 1.Auflage, Carl Hanser Verlag 1972, ISBN 3-446-11539-0
  • Ch. Wagenknecht: Algorithmen und Komplexität, 1.Auflage, Carl Hanser Verlag 2003, ISBN 3-446-22314-2
  • A. Iwainsky: Dynamische Optimierung - Gegenstand-Methoden-Möglichkeiten, 1.Auflage, VEB Verlag Technik Berlin 1984
  • J. Hinrichsen: Branch- and Bound-Verfahren zur Lösung des Rundreiseproblems, Vandenhoeck & Ruprecht Göttingen 1975, ISBN 3-525-12432-5
  • S. Wolfgang, U. Gerhard: Dissertation Duale Entartung in der lineare Optimierung, Transportproblem mit zusätzlichen Nebenbedingungen und Rundreiseproblem TU Dresden und Bergakademie Freiberg, 21.02.1975