Dynamische Programmierung und Divide-and-Conquer-Ähnlichkeiten
Aus meiner Sicht kann ich sagen, dass dynamische Programmierung eine Erweiterung des Paradigmas der Teilung und Eroberung ist .
Ich würde sie nicht als etwas völlig anderes behandeln. Weil beide arbeiten, indem sie ein Problem rekursiv in zwei oder mehr Unterprobleme desselben oder verwandten Typs aufteilen, bis diese einfach genug werden, um direkt gelöst zu werden. Die Lösungen für die Unterprobleme werden dann kombiniert, um eine Lösung für das ursprüngliche Problem zu erhalten.
Warum haben wir dann immer noch andere Paradigmennamen und warum habe ich dynamische Programmierung als Erweiterung bezeichnet? Dies liegt daran, dass der dynamische Programmieransatz nur dann auf das Problem angewendet werden kann, wenn das Problem bestimmte Einschränkungen oder Voraussetzungen aufweist . Und danach erweitert die dynamische Programmierung den Divide and Conquer-Ansatz um Memoization- oder Tabellierungstechniken .
Lass uns Schritt für Schritt gehen ...
Dynamische Programmiervoraussetzungen / -einschränkungen
Wie wir gerade entdeckt haben, gibt es zwei Schlüsselattribute, die das Problem teilen und überwinden muss, damit die dynamische Programmierung anwendbar ist:
Optimale Unterstruktur - Eine optimale Lösung kann aus optimalen Lösungen ihrer Teilprobleme konstruiert werden
Überlappende Teilprobleme - Das Problem kann in Teilprobleme unterteilt werden, die mehrmals wiederverwendet werden, oder ein rekursiver Algorithmus für das Problem löst das gleiche Teilproblem immer wieder, anstatt immer neue Teilprobleme zu generieren
Sobald diese beiden Bedingungen erfüllt sind, können wir sagen, dass dieses Teilungs- und Eroberungsproblem unter Verwendung eines dynamischen Programmieransatzes gelöst werden kann.
Dynamische Programmiererweiterung für Teilen und Erobern
Der dynamische Programmieransatz erweitert den Divide and Conquer-Ansatz um zwei Techniken ( Memoisierung und Tabellierung ), die beide dazu dienen, Teilproblemlösungen zu speichern und wiederzuverwenden, die die Leistung drastisch verbessern können. Zum Beispiel hat die naive rekursive Implementierung der Fibonacci-Funktion eine zeitliche Komplexität, O(2^n)
bei der die DP-Lösung dasselbe nur mit der O(n)
Zeit tut .
Das Speichern (Top-Down-Cache-Füllen) bezieht sich auf die Technik des Zwischenspeicherns und Wiederverwendens zuvor berechneter Ergebnisse. Die gespeicherte fib
Funktion würde also so aussehen:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
Die Tabellierung (Bottom-Up-Cache-Füllung) ist ähnlich, konzentriert sich jedoch auf das Füllen der Cache-Einträge. Die Berechnung der Werte im Cache erfolgt am einfachsten iterativ. Die Tabellenversion von fib
würde folgendermaßen aussehen:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Weitere Informationen zum Speichern und Vergleichen von Tabellen finden Sie hier .
Die Hauptidee, die Sie hier verstehen sollten, ist, dass das Zwischenspeichern von Teilproblemlösungen möglich wird, da unser Teilungs- und Eroberungsproblem überlappende Unterprobleme aufweist, und somit das Auswendiglernen / Tabellieren in die Szene vordringt.
Also, was ist der Unterschied zwischen DP und DC?
Da wir jetzt mit den DP-Voraussetzungen und ihren Methoden vertraut sind, sind wir bereit, alles, was oben erwähnt wurde, in einem Bild zusammenzufassen.
Wenn Sie Codebeispiele anzeigen möchten, finden Sie hier eine ausführlichere Erläuterung, in der Sie zwei Algorithmusbeispiele finden: Binäre Suche und Mindestbearbeitungsabstand (Levenshtein-Abstand), die den Unterschied zwischen DP und DC veranschaulichen.