rev4: Ein sehr beredter Kommentar von Benutzer Sammaron hat festgestellt, dass diese Antwort möglicherweise zuvor Top-Down und Bottom-Up verwechselt hat. Während ursprünglich diese Antwort (rev3) und andere Antworten besagten, dass "Bottom-up Memoization" ist ("die Teilprobleme annehmen"), kann es umgekehrt sein (das heißt, "top-down" kann "die Unterprobleme annehmen" und " bottom-up "kann" die Teilprobleme zusammensetzen "). Zuvor habe ich gelesen, dass Memoization eine andere Art der dynamischen Programmierung ist als ein Subtyp der dynamischen Programmierung. Ich habe diesen Standpunkt zitiert, obwohl ich ihn nicht abonniert habe. Ich habe diese Antwort so umgeschrieben, dass sie der Terminologie nicht entspricht, bis in der Literatur die richtigen Referenzen gefunden werden können. Ich habe diese Antwort auch in ein Community-Wiki konvertiert. Bitte bevorzugen Sie akademische Quellen. Referenzenliste:} {Literatur: 5 }
Rekapitulieren
Bei der dynamischen Programmierung geht es darum, Ihre Berechnungen so zu ordnen, dass Doppelarbeit nicht neu berechnet wird. Sie haben ein Hauptproblem (die Wurzel Ihres Baums von Teilproblemen) und Unterprobleme (Teilbäume). Die Teilprobleme wiederholen sich typischerweise und überlappen sich .
Betrachten Sie zum Beispiel Ihr Lieblingsbeispiel für Fibonnaci. Dies ist der vollständige Baum der Teilprobleme, wenn wir einen naiven rekursiven Aufruf durchgeführt haben:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(Bei einigen anderen seltenen Problemen kann dieser Baum in einigen Zweigen unendlich sein, was eine Nichtbeendigung darstellt, und daher kann die Unterseite des Baums unendlich groß sein. Bei einigen Problemen wissen Sie möglicherweise nicht, wie der vollständige Baum vor Ihnen aussieht Daher benötigen Sie möglicherweise eine Strategie / einen Algorithmus, um zu entscheiden, welche Teilprobleme aufgedeckt werden sollen.)
Auswendiglernen, Tabellieren
Es gibt mindestens zwei Haupttechniken der dynamischen Programmierung, die sich nicht gegenseitig ausschließen:
Auswendiglernen - Dies ist ein Laissez-Faire-Ansatz: Sie gehen davon aus, dass Sie bereits alle Teilprobleme berechnet haben und keine Ahnung haben, wie die optimale Bewertungsreihenfolge lautet. In der Regel führen Sie einen rekursiven Aufruf (oder ein iteratives Äquivalent) von der Wurzel aus aus und hoffen entweder, dass Sie der optimalen Bewertungsreihenfolge nahe kommen, oder Sie erhalten einen Beweis dafür, dass Sie dabei helfen, die optimale Bewertungsreihenfolge zu erreichen. Sie würde sicherstellen , dass der rekursive Aufruf neu berechnet nie ein Teilproblem , weil Sie zwischenspeichern , die Ergebnisse und damit doppelte Unterbäume sind nicht neu berechnet.
- Beispiel: Wenn Sie die Fibonacci-Sequenz berechnen
fib(100)
, würden Sie dies einfach aufrufen, und es würde aufrufen fib(100)=fib(99)+fib(98)
, was aufrufen würde fib(99)=fib(98)+fib(97)
, ... etc ..., was aufrufen würde fib(2)=fib(1)+fib(0)=1+0=1
. Dann würde es sich endgültig auflösen fib(3)=fib(2)+fib(1)
, aber es muss nicht neu berechnet werden fib(2)
, da wir es zwischengespeichert haben.
- Dies beginnt am oberen Rand des Baums und bewertet die Teilprobleme von den Blättern / Teilbäumen zurück zur Wurzel.
Tabellierung - Sie können sich dynamische Programmierung auch als einen "Tabellenfüll" -Algorithmus vorstellen (obwohl diese 'Tabelle' normalerweise mehrdimensional ist, kann sie in sehr seltenen Fällen eine nichteuklidische Geometrie aufweisen *). Dies ist wie das Auswendiglernen, jedoch aktiver und umfasst einen zusätzlichen Schritt: Sie müssen im Voraus die genaue Reihenfolge auswählen, in der Sie Ihre Berechnungen durchführen. Dies sollte nicht bedeuten, dass die Reihenfolge statisch sein muss, sondern dass Sie viel flexibler sind als das Auswendiglernen.
- Beispiel: Wenn Sie Fibonacci ausführen, Sie können wählen , die Zahlen in dieser Reihenfolge berechnen:
fib(2)
, fib(3)
, fib(4)
... Cachen jeden Wert , so dass Sie die nächsten sind leicht mehr berechnen kann. Sie können sich das auch als Ausfüllen einer Tabelle vorstellen (eine andere Form des Caching).
- Ich persönlich höre das Wort "Tabellierung" nicht oft, aber es ist ein sehr anständiger Begriff. Einige Leute betrachten diese "dynamische Programmierung".
- Vor dem Ausführen des Algorithmus betrachtet der Programmierer den gesamten Baum und schreibt dann einen Algorithmus, um die Teilprobleme in einer bestimmten Reihenfolge in Richtung der Wurzel zu bewerten, wobei im Allgemeinen eine Tabelle ausgefüllt wird.
- * Fußnote: Manchmal ist die 'Tabelle' per se keine rechteckige Tabelle mit gitterartiger Konnektivität. Vielmehr kann es eine kompliziertere Struktur haben, wie z. B. einen Baum oder eine Struktur, die für die Problemdomäne spezifisch ist (z. B. Städte in Flugentfernung auf einer Karte), oder sogar ein Gitterdiagramm, das zwar gitterartig, aber nicht vorhanden ist Eine Konnektivitätsstruktur von oben nach unten nach links nach rechts usw. Beispielsweise hat user3290797 ein dynamisches Programmierbeispiel verknüpft, um die maximale unabhängige Menge in einem Baum zu finden , die dem Ausfüllen der Lücken in einem Baum entspricht.
(An es ist allgemeinsten, in einem „dynamischen Programmierung“ Paradigma, würde ich sagen , dass der Programmierer den ganzen Baum hält, dannschreibt einen Algorithmus, der eine Strategie zur Bewertung von Teilproblemen implementiert, mit der die gewünschten Eigenschaften optimiert werden können (normalerweise eine Kombination aus Zeitkomplexität und Raumkomplexität). Ihre Strategie muss irgendwo mit einem bestimmten Teilproblem beginnen und kann sich möglicherweise basierend auf den Ergebnissen dieser Bewertungen anpassen. Im allgemeinen Sinne der "dynamischen Programmierung" könnten Sie versuchen, diese Teilprobleme zwischenzuspeichern, und generell versuchen, ein erneutes Aufrufen von Teilproblemen zu vermeiden, wobei eine subtile Unterscheidung möglicherweise bei Diagrammen in verschiedenen Datenstrukturen der Fall ist. Sehr oft sind diese Datenstrukturen im Kern wie Arrays oder Tabellen. Lösungen für Teilprobleme können weggeworfen werden, wenn wir sie nicht mehr brauchen.)
[Zuvor gab diese Antwort eine Erklärung zur Top-Down- und Bottom-Up-Terminologie ab. Es gibt eindeutig zwei Hauptansätze, die als Memoisierung und Tabellierung bezeichnet werden und möglicherweise mit diesen Begriffen in Konflikt stehen (wenn auch nicht vollständig). Der allgemeine Begriff, den die meisten Leute verwenden, ist immer noch "Dynamische Programmierung" und einige Leute sagen "Memoization", um sich auf diesen bestimmten Subtyp von "Dynamic Programming" zu beziehen. Diese Antwort lehnt es ab zu sagen, was von oben nach unten und von unten nach oben ist, bis die Community in wissenschaftlichen Arbeiten die richtigen Referenzen finden kann. Letztendlich ist es wichtig, die Unterscheidung und nicht die Terminologie zu verstehen.]
Vor-und Nachteile
Einfache Codierung
Das Auswendiglernen ist sehr einfach zu codieren (Sie können im Allgemeinen * eine "Memoizer" -Anmerkung oder eine Wrapper-Funktion schreiben, die dies automatisch für Sie erledigt) und sollte Ihre erste Vorgehensweise sein. Der Nachteil der Tabellierung ist, dass Sie eine Bestellung erstellen müssen.
* (Dies ist eigentlich nur einfach, wenn Sie die Funktion selbst schreiben und / oder in einer unreinen / nicht funktionierenden Programmiersprache codieren. Wenn beispielsweise jemand bereits eine vorkompilierte fib
Funktion geschrieben hat, führt dies notwendigerweise rekursive Aufrufe an sich selbst aus Sie können die Funktion nicht auf magische Weise auswendig lernen, ohne sicherzustellen, dass diese rekursiven Aufrufe Ihre neue gespeicherte Funktion aufrufen (und nicht die ursprüngliche nicht gespeicherte Funktion).
Rekursivität
Beachten Sie, dass sowohl von oben nach unten als auch von unten nach oben durch Rekursion oder iteratives Füllen von Tabellen implementiert werden kann, obwohl dies möglicherweise nicht natürlich ist.
Praktische Bedenken
Wenn der Baum beim Auswendiglernen sehr tief ist (z. B. fib(10^6)
), wird Ihnen der Stapelspeicherplatz ausgehen, da jede verzögerte Berechnung auf den Stapel gelegt werden muss und Sie 10 ^ 6 davon haben.
Optimalität
Beide Ansätze sind möglicherweise nicht zeitoptimal, wenn die Reihenfolge, in der Sie Teilprobleme besuchen (oder versuchen), nicht optimal ist, insbesondere wenn es mehr als eine Möglichkeit gibt, ein Teilproblem zu berechnen (normalerweise würde das Caching dies beheben, aber es ist theoretisch möglich, dass das Caching dies tut nicht in einigen exotischen Fällen). Das Auswendiglernen erhöht normalerweise Ihre Zeitkomplexität zu Ihrer Raumkomplexität (z. B. haben Sie bei der Tabellierung mehr Freiheit, Berechnungen wegzuwerfen, z. B. bei der Tabellierung mit Fib können Sie O (1) -Raum verwenden, bei der Memoisierung bei Fib wird jedoch O (N) verwendet. Stapelplatz).
Erweiterte Optimierungen
Wenn Sie auch extrem komplizierte Probleme haben, haben Sie möglicherweise keine andere Wahl, als eine Tabellierung durchzuführen (oder zumindest eine aktivere Rolle bei der Steuerung der Memoisierung zu übernehmen, wo Sie sie haben möchten). Auch wenn Sie sich in einer Situation befinden, in der die Optimierung absolut kritisch ist und Sie optimieren müssen, können Sie mithilfe der Tabellierung Optimierungen vornehmen, die Sie sonst durch Memoisierung nicht auf vernünftige Weise durchführen würden. Meiner bescheidenen Meinung nach taucht in der normalen Softwareentwicklung keiner dieser beiden Fälle jemals auf, daher würde ich nur Memoization ("eine Funktion, die ihre Antworten zwischenspeichert") verwenden, es sei denn, etwas (wie z. B. Stapelspeicher) macht eine Tabellierung erforderlich Um ein Ausblasen des Stapels zu vermeiden, können Sie 1) die Stapelgrößenbeschränkung in Sprachen erhöhen, die dies zulassen, oder 2) einen konstanten Faktor zusätzlicher Arbeit für die Virtualisierung Ihres Stapels (ick) aufwenden,
Kompliziertere Beispiele
Hier listen wir Beispiele von besonderem Interesse auf, die nicht nur allgemeine DP-Probleme sind, sondern interessanterweise Memoisierung und Tabellierung unterscheiden. Beispielsweise kann eine Formulierung viel einfacher sein als die andere, oder es kann eine Optimierung geben, die grundsätzlich eine Tabellierung erfordert:
- der Algorithmus zur Berechnung der Bearbeitungsentfernung [ 4 ], interessant als nicht triviales Beispiel eines zweidimensionalen Algorithmus zum Füllen von Tabellen