dynamische Programmierung einfach erklärt?

Servus!

Ich muss im Rahmen meines Studiums eine Präsentation über dynamische Programmierung halten. Wir haben zwar in einer Übung ein wenig damit zu tun gehabt aber so wirklich durchschaut habe ich das Prinzip nicht. Im Internet findet man zwar jede Menge Unterlagen, Großteils aber von technischen Universitäten die ich absolut nicht durchblicke. Muss ich erst einen Abschluss in Mathematik haben bevor ich dynamische Programmierung durchschaue? Selbst auf Wikipedia sind der Erklärungen nicht wirklich "deutsch".

Was ist z.B. beim Rucksackproblem so schlecht an dem Algorithmus:
- Berechne für jeden Gegenstand das Wert/Gewichts-Verhältnis
- Sortiere die Gegenstände nach diesem Verhältnis
- Nehme den jeweils obersten Gegenstand der Liste bis der nächste Gegenstand nicht mehr in den Rucksack passt.
- Überspringe diesen Gegenstand, nehme den nächstbesten der passt
- Wiederhole so lange bis entweder der Rucksack voll ist oder keine Gegenstände mehr da sind.

Bzw. was wird bei der dynamischen Programmierung anders gemacht? Ich hätte gerne eine Erklärung die so einfach wie möglich ist, damit sie auch Nicht-Informatiker und -Mathematiker verstehen ...
 
Was du beschreibts ist ein Greedy-Algorithmus. Der kann in manchen Fällen optimale Lösungen produzieren (siehe "Greedy-Property") aber eben nicht immer. Dynamische Programmierung bedeutet im Grunde, dass man alle möglichen Lösungen probiert und dabei speichert, was man an "Unterlösungen" schonmal hatte. Beim nächsten Versuch probiert man diese Unterlösung nicht nochmal, da man sie ja schon gespeichert hatte und dann dadurch bewertet.

Als Lektüre kann ich da "Introduction to Algorithms" empfehlen, wo ein ganzes Kapitel der dynamischen Programmierung gewidmet wird.
 
Hey

ich kann mich Buch-Technisch bad_alloc nur anschließen! Jedoch gibt es von MIT und Stanford jeweils open-courses - einfach einschalten, zurücklehnen und belehren lassen :wink:

Stanford

MIT

Viel Spaß :)

Christoph
 
Die häufigsten dyn. Prog. Anwendungsmöglichkeiten habe ich eher in Graphentheorie gesehen (Rucksackproblem kann auch darauf abgebildet werden). Was aber der Anschaulichkeit für "nicht-Nerdler" doch eher abträglich ist ;). Rucksackproblem ist daher schon mal nicht schlecht.

Zu Deinem Algo:
Was ist z.B. beim Rucksackproblem so schlecht an dem Algorithmus:
- Berechne für jeden Gegenstand das Wert/Gewichts-Verhältnis
- Sortiere die Gegenstände nach diesem Verhältnis
- Nehme den jeweils obersten Gegenstand der Liste bis der nächste Gegenstand nicht mehr in den Rucksack passt.
- Überspringe diesen Gegenstand, nehme den nächstbesten der passt
- Wiederhole so lange bis entweder der Rucksack voll ist oder keine Gegenstände mehr da sind.
Gegenbeispiel: bei einem Gewinnspiel "ein Karton vom Laster" hat man folgende Geräte zur Auswahl:

Asis Netbook: 1kg, 600 Euro, W/G = 600
Leenovo Thunkpad: 2kg, 1000 Euro, W/G=500
Birne MucBack Pro: 3kg, 1200 Euro, W/G = 400

Und eine Mini-Frauenhandtasche, die nur 5kg tragen kann(daher auch Mini ;) ).

Nach Deinem Algo:
600,500,400:
liefert:
Asis Netbook und Leenovo Thunkpad mit einem Gesamtwert von 1600 Euro

Optimal:
Leenovo und Birne MucBack Pro: 2200 Euro

;----------------------
Greedy findet nur ein lokales Optimum. Anders gesagt: man findet (recht schnell) eine Lösung, weiß aber nicht, wie gut diese ist.

Um optimale Lösung zu finden, muss man alle Lösungsmöglichkeiten betrachten. Und hier gibt es den Unterschied zwischen "dynamisch" und "naiv/normal/rekursiv/backtracking".

Naive Lösung:
Bilde Kombinationen und merke die beste.
Code:
<typischer Backtrackingalgo für Rucksackproblem einsetz>
oder:
Nehme Gegenstand1, packe in den Rucksack
Nehme Gegenstand2 ... bis Rucksack voll.
Merke Lösung
Packe letzten Gegenstand aus,packe einen anderen rein
Merke Lösung, falls besser als letzte
Packe letzen Gegenstand aus ... bis alle durchprobiert wurden
Packe die beiden letzen Gegenstände aus, packe nun einen anderen Gegenstand ein (usw, bis man wieder beim leeren Rucksack angekommen ist und nun mit
"Nehme Gegenstand2, packe diesen in den Rucksack" anfängt).
Graphisch schaut das so aus:
Code:
                            _+G1_________{}________+G4
                     ___-{G1}           | | \.\       \
                +G2 /   +G2| \...      ... ...  ...   {G4}
                  /        |                         /||||\ +G1
            {G1,G2}       {G1,G3}                   .......{G4,G1}
     +G3  /  |    \ \     / |                              /|||||\ + G2
         /   |+G4  \ .....  | +G2                        ....... {G4,G1,G2}
G1,G2,G3}    |      \      {G1,G3,G2}
         {G1,G2,G4} ...
"..." sind weggelassene Abzweigungen.

Wie man unschwer erkennen kann, werden hierbei oft gleiche Lösungen mehrfach generiert/betrachtet (die Gegenstände kommen in einer anderen Reihenfolge in den Rucksack).
{G1,G2,G3} == {G1,G3,G2} == {G2,G3,G1}
Laufzeit: Pi *Daumen * (Gesamtanzahl der Gegenstände ^ (max. Pfadtiefe bzw. Gegenstände, die das max. Gewicht erreichen)).
Also Exponentiell.

Edit:
Eigentlich würde ja ausreichen, nur einen kleinen Teil der möglichen Pfade zu verfolgen - der Rest ist "dasselbe in grün/blau/rot".
Da müsste man aber immer noch alle Kombinationen bis zum zulässigen Gewicht ausrechnen, was imho in die Richtung von Binomialkoeffizint
("Gesamt_gegenstände" über "max. anzahl die in den Rucksack passt") oder wie beim Lotto: (bei 50 Gegenständen) "x aus 50".
/Edit

Prinzipiell spricht nichts dagegen, den naiven Backtracking Algoritmus mit einer Datenbank auszustatten, die bereits berechnete Ergebnisse speichert. Wichtig wäre nur, dass die Reihenfolge der Gegenstände nicht beachtet wird (also einfach ein Set). Auf "neuprogrammisch" kann man auch vom "memoizing" des Algos sprechen.

Dynamische Programmierung:
[OT]
Der "i+j" Algo"kram" aus Wikipedia ist ein Tribut an die uncoolen Zeiten, als der Speicher noch rar und teuer war und die Algos auf die Lochkarten gestanzt werden mussten * :evil: * [/OT]

Ernsthaft: während bei der "naiven" Methode man "von oben nach unten" geht, berechnet man bei dynamischer Programmierung Teillösungen und setzt diese zu einer Lösung zusammen.
Code:
wikipedia:
Eingabe: U = 1..n Gegenstände, B(ound), w(eight), v(alue) [FONT=monospace]
[/FONT]R := [1…(n+1), 0…B]-Matrix, mit Einträgen 0
FOR i = n … 1
    FOR j = 1 … B
        IF w(i) <= j[FONT=verdana]
            [/FONT]R[i,j] := max( v(i) + R[i+1, j-w(i)], R[i+1,j] )[FONT=monospace]
     [/FONT]ELSE[FONT=verdana]
            [/FONT]R[i,j] := R[i+1,j][FONT=verdana]

[/FONT]Ausgabe: R[1,B]

1 to n sind Items. Wie man sieht, wird die Liste nur 1 mal durchlaufen.
0 to W sind ganzzahlige Gewichte. Wenn also 50kg das Maximum sind:
1,2,3 ... 49,50

Anders gesagt: für jeden Gegenstand 1..n werden die möglichen Gewichte betrachtet und nach maximalen Nutzwert ausschau gehalten:
Code:
Gew/Gegenstand: 
  3 2 1
1 0 0 0
2 0 0 0
3 0 0 0
Code:
[/FONT][FONT=monospace]
[/FONT]R := [1…(n+1), 0…Max_gewicht]-Matrix, mit Einträgen 0
FOR Gegenstand aus Liste:
    FOR kg = 1 … Max_gewicht
        IF Gewicht(Gegenstand) <= kg[FONT=verdana]
            [/FONT]R[Gegenstand,kg] := max(Preis(Gegenstand) + R[vorheriger_gegenstand, kg-Gewicht(Gegenstand)], R[vorheriger_gegenstand,kg] )[FONT=monospace]
     [/FONT]ELSE[FONT=verdana]
            [/FONT]R[Gegenstand,kg] := R[vorhergiger_gegenstand,kg][FONT=verdana]

[/FONT]Ausgabe: R[letzter_gegenstand,Max_gewicht]
Die Lösung ergibt sich also aus den vorberechneten Teillösungen (z.B Eintrag für alle Gewichte des vorherigen Gegenstands)
vielleicht ist das hier etwas anschaulicher: http://polya.computing.dcu.ie/courses/ca313/w6/index.html
 
Danke für die Antworten! Echt super hier :thumb_up:

Mein Kopf raucht noch ein wenig von der MIT Vorlesung.

Das Beispiel von CDW ist einleuchtend, auch das mit den Suchbäumen. Ich glaub das werde ich für meine Präsentation klauen ;)

Kann man also sagen, dass man beim dynamische Ansatz das Rucksackproblem zu lösen auch alle Lösung durchprobiert, nur eben auch alle Lösungen in einer Tabelle (Array, Datenbank, ...) speichert und bevor man eine Lösung berechnet kontrolliert ob es diese Kombination schon gab und dann gleich diesen Wert einsetzt? Falls ja: Was ist daran so viel schneller? Oder höre ich dann im Suchbaum direkt auf wenn ich auf eine schon berechnete Kombination stoße?

Edit: Wenn man darüber nachdenkt war meine Fragestellung sehr dumm :-| Natürlich höre ich auf weil wenn ich z.B. {G1,G2,G4} und {G4,G2,G1} habe brauch ich nur noch bei einem von beiden weitermachen; Zwei Iterationen später wäre {G1,G2,G4,G3,G5} ja wieder das selbe wie {G4,G2,G1,G3,G5} ... sprich es kann keine Kombination {G4,G2,G1,...} geben die nicht auch in {G1,G2,G4,...} enthalten ist.
 
Zuletzt bearbeitet:
Meine Abschätzung für "memoizing" war im übrigen falsch.
Wenn man es überlegt: man spart sich nur die "mehrfach gleiche" Kombinationen ein, muss aber trotzdem "x aus Anz. der Gegenstände" betrachten. Man "sägt" quasi die Äste im Suchbaum weg ;)
Ich komme zur aktuellen Uhrzeit auf Binomialkoeffizienten als Laufzeit, was immer noch exponentiell sein sollte :( [0]

Man sollte aber beachten, dass die Wiki-Beispiele (sowie die meisten Folien) nur einen Algo präsentieren, der die optimale Nutzkapazität ausrechnet (ohne konkerete Gegenstände als Lösung).

Der Unterschied zu der "naiven" Lösung (mit "memoizing" oder ohne) ist der "divide & conquer" vs "bottom up". Man berechnet also nicht die Lösung, indem man das Problem aufteilt und davon Teillösungen berechnet.
Sondern startet mit optimalen "Mini-Lösungen", aus denen sich eine größere zusammensetzen lässt.
Algorithms/Dynamic Programming - Wikibooks, open books for an open world

Hier (Rucksack):
man bildet einen Intervall mit 1 Schrittweite von 0 bis Max.Gewicht.
Das nutzt man als Spalten einer Tabelle
Die Gegenstände bilden die Zeilen.
Nun berechnet man für jeden Gewichtsspalte für jeweiligen Gegenstand die "bestmögliche" Belegung (aus allen vorherigen Gegenständen) für das jeweilige Gewicht. Wie gesagt, am besten setzt man
Dynamic Programming
und den Wikipediaalgo nebeneinander:
Code:
KS(c, i)
// c = remaining capacity, i is number of remaining items that we are considering.
   if i > n
      return 0 // No more items remaining

   if w[i] > c
      // this item is too big, can't include it, try next item
      return KS(c, i + 1)
   else
      return max(KS(c, i + 1), v[i] + KS(c - w[i], i + 1)
      // If we include the item, then get its value v[i] but reduce capacity w[i]
Code:
[B]for[/B] w [B]from[/B] 0 [B]to[/B] W [B]do[/B]
  T[0, w] := 0
[B]end for[/B]

[B]for[/B] i [B]from[/B] 1 [B]to[/B] n [B]do[/B]
  [B]for[/B] j [B]from[/B] 0 [B]to[/B] W [B]do[/B]
    [B]if[/B] j >= w[i] [B]then[/B]
      T[i, j] := max(T[i-1, j], T[i-1, j-w[i]] + v[i])
    [B]else[/B]
      T[i, j] := T[i-1, j]

    [B]end if[/B]
  [B]end for[/B]
[B]end for[/B]
[0]andererseits ist es ein NP Problem. Sollte also nicht verwunderlich sein, wenn man nicht von den "horrenden" Laufzeiten wegkommen kann *hust*
 
Hey,

wiedermal sehr schöne Erklärungen von CDW! :thumb_up:

Ich verweise hier mal auf ein Skript, dass ich vor einiger Zeit mal hier hochgeladen habe. Es handelt sich dabei um eine gewisse Zusammenfassung des Buches, was bad_alloc vorgeschlagen hat. Eventuell kann dir das weiterhelfen, auch in Hinblick auf gewisse Grundverständnisse! So solltest du mal einen divide-and-conquer Algorithmus gesehen und verstanden haben, bevor du dich mit der dynamischen Programmierung rumschlägst - auch eine Trennung der Begriffe ist hilfreich. Natürlich kann ich dir nur raten, dir das Buch zu holen (ist auch auf Deutsch erhältlich) und einfach die Kapitel durchzulesen (die, die du schon gesehen hast, kannst du natürlich überfliegen) um dich dann fundiert deinem Thema zu widmen.

Hier wird jetzt sehr stark über die Implementierung eines Algorithmus für das Rucksack-Problem diskutiert, du solltest aber nicht vergessen, dass das Thema "Dynamische Programmierung" ist - worin etwas mehr mitschwingt als ein NP-Problem, bei dem dynamische Programmierung wieder "nur" einen exponentiellen Algorithmus erzeugt. Dadurch wird das Beispiel für mich etwas unbedeutend im Bezug auf deine Themenstellung. Ich finde z.B. die Fibonacci-Zahl Berechnung aus CDW's sehr viel anschaulicher, weil das "Problem" bekannt ist und man verschiedene Techniken vorstellen kann, die man auch als Zuhörer sofort versteht...aber ist nur meine Meinung :D

Grüße

Scutus
 
Mittlerweile wurdest Du ja mit guten Quellen überhäuft und auch sonst sehr gut beraten, aber da es Leute gibt die komplexe Themen am liebsten auf Deutsch vertiefen, hier noch Script zum Thema von der Uni Dortmund:
lrb.cs.uni-dortmund.de/~tick/Lehre/SS08/Proseminar/07-schaefer.pdf
 
Das von der Uni Dortmund hatte ich schon per Google gefunden und ist für mich nicht so verständlich. Nachdem ich das am Dienstag wissen sollte wird sich das mit dem Buch auch nicht mehr ausgehen ...

Okay, Rucksackproblem ist zwar für den Laien gut nachvollziehbar aber leider auch bei dynamischer Programmierung noch mit exponentiellem Rechenaufwand verbunden. Ist das nicht bei der Fibonacci-Zahl (wikibook) das selbe wenn ich den Baum bilde und dann die schon berechneten Äste streiche?
Und was ist mit der, auch im wikibook angesprochenen Methode, dass ich einfach eine Schleife habe und jedes mal von f(0) weg alle Fibonacci-Zahlen berechne ("Bottom-Up"). Dann habe ich kein Problem mit doppelten Ästen und ich habe immer nur zwei Zahlen im Speicher. Ist dieser Ansatz überhaupt "dynamische Programmierung"?

Vermutlich nerve ich schon mit meinen Fragen :( Finde es echt super, und alles andere als selbstverständlich, welche Mühe ihr euch gebt! In anderen Foren gäbe es wohl nur ein paar Links zu pdfs welche ich selber auch schon gefunden hatte.
 
Okay, Rucksackproblem ist zwar für den Laien gut nachvollziehbar aber leider auch bei dynamischer Programmierung noch mit exponentiellem Rechenaufwand verbunden.
Ja, das ist halt das Problem mit den NP-Problemen ;)
Bei dynamischer Programmierung (vs memoization) kann man aber z.B Speicherverbrauch sparen - zumindest bei Fibonacciberechnung.

Ist das nicht bei der Fibonacci-Zahl (wikibook) das selbe wenn ich den Baum bilde und dann die schon berechneten Äste streiche?
Laufzeittechnisch prinzipiell schon - Speicherverbrauch ist aber dann linear von der Eingabegröße abhängig - man speichert "konstante"*N Berechnungen.
bei dynamischer Vorgehnsweise speichert man nur 2 Werte.

Und was ist mit der, auch im wikibook angesprochenen Methode, dass ich einfach eine Schleife habe und jedes mal von f(0) weg alle Fibonacci-Zahlen berechne ("Bottom-Up"). Dann habe ich kein Problem mit doppelten Ästen und ich habe immer nur zwei Zahlen im Speicher. Ist dieser Ansatz überhaupt "dynamische Programmierung"?
Jep, ist es.
Eventuell ist bei diesem Beispiel (gegenüber Rucksackproblem) verwirrend, dass Fibonacci kein NP Problem ist - man kann es zwar mit einem Algorithmus lösen, der Exp-Zeit braucht, muss man aber nicht.
OT:
Die "naive" Fibonaccilösung wird gerne als Rekursionseinführung genommen - und ebenso gerne von "Gegnern" der Rekursion aufgeführt ("hier eine iterative Lösung: viel viel viel schneller => also Rekursion voll lame!!1elf!") :)
 
Zurück
Oben