Dieser Artikel beschreibt die Ăhnlichkeiten und Unterschiede zwischen den beiden AnsĂ€tzen zur Lösung algorithmischer Probleme:
dynamische Programmierung (dynamische Programmierung) und das Prinzip
âTeilen und Erobernâ (Teilen und Erobern). Wir werden als Beispiel zwei Algorithmen vergleichen: die
binÀre Suche (wie man schnell eine Zahl in einem sortierten Array findet) und die
Levenshtein-Entfernung (wie man eine Zeile mit einer minimalen Anzahl von Operationen in eine andere konvertiert).
Ich möchte sofort darauf hinweisen, dass dieser Vergleich und diese ErklĂ€rung nicht den Anspruch erheben, Ă€uĂerst korrekt zu sein. Und vielleicht möchten mich sogar einige UniversitĂ€tsprofessoren ausschlieĂen :) Dieser Artikel ist nur mein persönlicher Versuch, Dinge zu klĂ€ren und zu verstehen, was dynamische Programmierung ist und wie das Prinzip âTeilen und Erobernâ involviert ist.Also, fangen wir an ...

Das Problem
Als ich
anfing, Algorithmen zu studieren, war es
fĂŒr mich schwierig, die Grundidee der dynamischen Programmierung (im Folgenden
DP aus der dynamischen Programmierung) zu verstehen und zu verstehen, wie sie sich vom Ansatz âTeilen und Erobernâ (weiter
DC , Teilen und Erobern) unterscheidet. Wenn es darum geht, diese beiden Paradigmen zu vergleichen, verwenden normalerweise
viele erfolgreich die Fibonacci-Funktion, um dies zu veranschaulichen. Und das ist eine groĂartige Illustration. Mir scheint jedoch, dass wir ein wichtiges Detail verlieren, wenn wir
dieselbe Aufgabe zur Veranschaulichung von DP und DC verwenden, um den Unterschied zwischen den beiden AnsÀtzen schneller zu erkennen. Und dieses Detail ist, dass sich diese beiden Techniken am besten bei der Lösung
verschiedener Arten von Problemen manifestieren.
Ich bin noch dabei, DP und DC zu lernen, und ich kann nicht sagen, dass ich diese Konzepte vollstÀndig verstanden habe. Ich hoffe jedoch weiterhin, dass dieser Artikel zusÀtzliches Licht ins Dunkel bringt und dazu beitrÀgt, den nÀchsten Schritt bei der Untersuchung so wichtiger AnsÀtze wie dynamisches Programmieren und Teilen und Erobern zu unternehmen.
Ăhnlichkeiten zwischen DP und DC
So wie ich diese beiden Konzepte jetzt sehe, kann ich schlieĂen, dass
DP eine erweiterte Version von DC ist .
Ich wĂŒrde sie
nicht als etwas völlig anderes betrachten. Da beide Konzepte
ein Problem rekursiv in zwei oder mehr Teilprobleme desselben Typs aufteilen, bis diese Teilprobleme leicht direkt zu lösen sind. Ferner werden alle Lösungen fĂŒr das Teilproblem miteinander kombiniert, um letztendlich eine Antwort auf das ursprĂŒngliche, ursprĂŒngliche Problem zu geben.
Warum haben wir dann immer noch zwei verschiedene AnsÀtze (DP und DC) und warum habe ich dynamische Programmierung als Erweiterung bezeichnet? Dies liegt daran, dass die dynamische Programmierung auf Aufgaben angewendet werden kann, die bestimmte
Eigenschaften und EinschrÀnkungen aufweisen . Und nur in diesem Fall erweitert DP DC mithilfe von zwei Techniken:
Memoisierung und
Tabellierung .
Gehen wir etwas tiefer in die Details ...
EinschrĂ€nkungen und Eigenschaften, die fĂŒr die dynamische Programmierung erforderlich sind
Wie wir gerade herausgefunden haben, gibt es zwei SchlĂŒsselmerkmale, die eine Aufgabe / ein Problem haben muss, damit wir versuchen können, es mithilfe dynamischer Programmierung zu lösen:
- Optimale Unterstruktur - Es sollte möglich sein, eine optimale Lösung fĂŒr ein Problem aus einer optimalen Lösung fĂŒr seine Unteraufgaben zusammenzustellen.
- Ăberschneidende Teilprobleme - Das Problem muss in Teilprobleme unterteilt werden, die wiederum wiederholt wiederverwendet werden. Mit anderen Worten, ein rekursiver Ansatz zur Lösung des Problems wĂŒrde eine mehrfache ( nicht eine einzige) Lösung fĂŒr dasselbe Teilproblem bedeuten, anstatt in jedem rekursiven Zyklus neue und eindeutige Teilprobleme zu erzeugen.
Sobald wir diese beiden Merkmale in dem betrachteten Problem finden, können wir sagen, dass es mit dynamischer Programmierung gelöst werden kann.
Dynamische Programmierung als Erweiterung des Prinzips "Teilen und Erobern"
DP erweitert DC mithilfe von zwei Techniken (
Memoisierung und
Tabellierung ), mit denen Lösungen fĂŒr Teilprobleme fĂŒr die zukĂŒnftige Wiederverwendung gespeichert werden sollen. Daher werden Lösungen nach Teilproblemen zwischengespeichert, was zu einer signifikanten Verbesserung der Algorithmusleistung fĂŒhrt. Beispielsweise ist die zeitliche KomplexitĂ€t einer "naiven" rekursiven Implementierung der Fibonacci-Funktion
O(2 n )
. Gleichzeitig wird eine auf dynamischer Programmierung basierende Lösung in nur
(n)
.
Das Auswendiglernen (FĂŒllen des Caches von oben nach unten) ist eine Caching-Technik, bei der neu berechnete Lösungen fĂŒr Unteraufgaben verwendet werden. Die Fibonacci-Funktion, die die Memoisierungstechnik verwendet, wĂŒrde folgendermaĂen 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 (FĂŒllen des Caches von unten nach oben) ist eine Ă€hnliche Technik, die sich jedoch in erster Linie darauf konzentriert, den Cache zu fĂŒllen und keine Lösung fĂŒr das Teilproblem zu finden. Die Berechnung der Werte, die zwischengespeichert werden mĂŒssen, ist in diesem Fall am einfachsten iterativ und nicht rekursiv durchzufĂŒhren. Die Fibonacci-Funktion unter Verwendung der Tabellentechnik 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 Vergleich von Memoisierung und Tabellierung finden Sie
hier .
Die Hauptidee, die in diesen Beispielen berĂŒcksichtigt werden muss, ist, dass wir, da unsere DC-Probleme ĂŒberlappende Teilprobleme haben, das Zwischenspeichern von Lösungen fĂŒr Teilprobleme mithilfe einer von zwei Zwischenspeichertechniken verwenden können: Memoisierung und Tabellierung.
Was ist also der Unterschied zwischen DP und DC am Ende?
Wir haben die EinschrĂ€nkungen und Voraussetzungen fĂŒr die Verwendung der dynamischen Programmierung sowie die im DP-Ansatz verwendeten Caching-Techniken kennengelernt. Versuchen wir, die obigen Gedanken in der folgenden Abbildung zusammenzufassen und darzustellen:

Lassen Sie uns versuchen, einige Probleme mit DP und DC zu lösen, um diese beiden AnsÀtze in Aktion zu demonstrieren.
Teilen und Erobern Beispiel: BinÀre Suche
Der binĂ€re Suchalgorithmus ist ein Suchalgorithmus, der die Position des angeforderten Elements in einem sortierten Array findet. Bei der binĂ€ren Suche vergleichen wir den Wert der Variablen mit dem Wert des Elements in der Mitte des Arrays. Wenn sie nicht gleich sind, kann die HĂ€lfte des Arrays, in dem sich das gewĂŒnschte Element befindet, nicht von der weiteren Suche ausgeschlossen werden. Die Suche wird in der HĂ€lfte des Arrays fortgesetzt, in der sich die gewĂŒnschte Variable befinden kann, bis sie gefunden wird. Wenn die nĂ€chste HĂ€lfte des Arrays keine Elemente enthĂ€lt, wird die Suche als abgeschlossen betrachtet und wir schlieĂen daraus, dass das Array nicht den gewĂŒnschten Wert enthĂ€lt.
BeispielDie folgende Abbildung zeigt ein Beispiel fĂŒr eine binĂ€re Suche nach der Nummer 4 in einem Array.

Lassen Sie uns dieselbe Suchlogik darstellen, jedoch in Form eines âEntscheidungsbaumsâ.

Sie können in diesem Diagramm ein klares Prinzip von "Teilen und Erobern" sehen, das zur Lösung dieses Problems verwendet wird. Wir teilen unser ursprĂŒngliches Array iterativ in Subarrays auf und versuchen, das Element, nach dem wir suchen, bereits darin zu finden.
Können wir dieses Problem mit dynamischer Programmierung lösen?
Nein, nein. Aus dem Grund, dass diese Aufgabe
keine sich ĂŒberschneidenden Teilprobleme enthĂ€lt . Jedes Mal, wenn wir ein Array in Teile aufteilen, sind beide Teile völlig unabhĂ€ngig und ĂŒberlappen sich nicht. Und gemÀà den oben diskutierten Annahmen und EinschrĂ€nkungen der dynamischen Programmierung mĂŒssen sich Teilprobleme irgendwie ĂŒberschneiden, sie
mĂŒssen sich wiederholen .
Wenn ein Entscheidungsbaum genau wie ein Baum aussieht (und
nicht wie ein Diagramm ), bedeutet dies normalerweise höchstwahrscheinlich, dass es keine ĂŒberlappenden Teilprobleme gibt.
Implementierung des AlgorithmusHier finden Sie den vollstÀndigen Quellcode des binÀren Suchalgorithmus mit Tests und ErklÀrungen.
function binarySearch(sortedArray, seekElement) { let startIndex = 0; let endIndex = sortedArray.length - 1; while (startIndex <= endIndex) { const middleIndex = startIndex + Math.floor((endIndex - startIndex) / 2);
Beispiel fĂŒr eine dynamische Programmierung: Bearbeiten der Entfernung
Wenn es um die ErklÀrung der dynamischen Programmierung geht, wird normalerweise
die Fibonacci-Funktion als Beispiel verwendet. Nehmen wir in unserem Fall ein etwas komplexeres Beispiel. Je mehr Beispiele, desto einfacher ist es, das Konzept zu verstehen.
Der Bearbeitungsabstand (oder der Levenshtein-Abstand) zwischen zwei Zeilen ist die Mindestanzahl von Operationen, um ein Zeichen einzufĂŒgen, ein Zeichen zu löschen und ein Zeichen durch ein anderes zu ersetzen, die erforderlich sind, um eine Zeile in eine andere umzuwandeln.
BeispielDer Bearbeitungsabstand zwischen den Wörtern "KĂ€tzchen" und "Sitzen" betrĂ€gt beispielsweise 3, da Sie drei BearbeitungsvorgĂ€nge (zwei Ersetzungen und eine EinfĂŒgung) ausfĂŒhren mĂŒssen, um eine Zeile in eine andere umzuwandeln. Und es ist unmöglich, eine schnellere Konvertierungsoption mit weniger VorgĂ€ngen zu finden:
- KĂ€tzchen â sitten (Ersetzen von "k" durch "s")
- sitten â sittin (ersetzt "e" durch "i")
- sittin â sitzend (âgâ vollstĂ€ndig einfĂŒgen).
AlgorithmusanwendungDer Algorithmus hat eine breite Palette von Anwendungen, zum Beispiel fĂŒr die RechtschreibprĂŒfung, optische Erkennungskorrektursysteme, ungenaue Zeichenfolgensuche usw.
Mathematische Definition eines ProblemsMathematisch wird der Levenstein-Abstand zwischen zwei Linien
a, b
(mit den LĂ€ngen | a | bzw.
|b|
) durch die
function lev(|a|, |b|)
, wobei:

Bitte beachten Sie, dass die erste Zeile in der
min
Funktion der
Löschoperation entspricht, die zweite Zeile der
EinfĂŒgeoperation und die dritte Zeile der
Ersetzungsoperation (falls die Buchstaben nicht gleich sind).
ErklĂ€rungVersuchen wir herauszufinden, was diese Formel uns sagt. Nehmen Sie ein einfaches Beispiel fĂŒr die Ermittlung des Mindestbearbeitungsabstands zwischen den Zeilen
ME und
MY . Intuitiv wissen Sie bereits, dass der minimale Bearbeitungsabstand ein (
1 ) Ersetzungsvorgang ist (ersetzen Sie âEâ durch âYâ). Aber lassen Sie uns unsere Lösung formalisieren und in eine algorithmische Form umwandeln, um komplexere Versionen dieses Problems zu lösen, beispielsweise die Umwandlung des Wortes
Samstag in
Sonntag .
Um die Formel auf die Transformation ME â MY anzuwenden, mĂŒssen wir zuerst den minimalen Bearbeitungsabstand zwischen ME â M, M â MY und M â M ermitteln. Als nĂ€chstes mĂŒssen wir das Minimum von drei AbstĂ€nden wĂ€hlen und eine Operation (+1) der Transformation E â Y hinzufĂŒgen.
Wir können also bereits die rekursive Natur dieser Lösung erkennen: Der minimale Bearbeitungsabstand ME â MY wird basierend auf den drei vorherigen möglichen Transformationen berechnet. Wir können also bereits sagen, dass dies ein Divide and Conquer-Algorithmus ist.
Um den Algorithmus weiter zu erlĂ€utern, fĂŒgen wir unsere beiden Zeilen in eine Matrix ein:
Zelle (0,1) enthĂ€lt die rote Zahl 1. Dies bedeutet, dass wir 1 Operation ausfĂŒhren mĂŒssen, um M in eine leere Zeichenfolge umzuwandeln - löschen Sie M. Daher haben wir diese Zahl rot markiert.
Zelle (0,2) enthĂ€lt eine rote Zahl 2. Dies bedeutet, dass wir zwei Operationen ausfĂŒhren mĂŒssen, um die Zeichenfolge ME in eine leere Zeichenfolge umzuwandeln - E löschen, M löschen.
Zelle (1,0) enthĂ€lt eine grĂŒne Zahl 1. Dies bedeutet, dass wir 1 Operation benötigen, um eine leere Zeichenfolge in M ââ- EinfĂŒgen M umzuwandeln. Wir haben die EinfĂŒgeoperation grĂŒn markiert.
Zelle (2,0) enthĂ€lt eine grĂŒne Zahl 2. Dies bedeutet, dass wir zwei Operationen ausfĂŒhren mĂŒssen, um eine leere Zeichenfolge in eine Zeichenfolge MY umzuwandeln - Y einfĂŒgen, M einfĂŒgen.
Zelle (1,1) enthĂ€lt die Nummer 0. Dies bedeutet, dass wir keine Operationen ausfĂŒhren mĂŒssen, um die Zeichenfolge M in M ââzu konvertieren.
Zelle (1,2) enthĂ€lt die rote Zahl 1. Dies bedeutet, dass wir 1 Operation ausfĂŒhren mĂŒssen, um den String ME in M ââ- delete E umzuwandeln.
UswâŠ
FĂŒr kleine Matrizen wie unsere (nur 3x3) sieht es nicht schwierig aus. Aber wie können wir die Werte aller Zellen fĂŒr groĂe Matrizen berechnen (zum Beispiel fĂŒr eine 9x7-Matrix in der Transformation Samstag â Sonntag)?
Die gute Nachricht ist, dass wir gemÀà der Formel nur die Werte von 3 benachbarten Zellen
(i-1,j)
,
(i-1,j-1)
(i-1,j)
berechnen mĂŒssen, um den Wert einer Zelle mit den Koordinaten
(i,j)
zu berechnen
(i-1,j-1)
und
(i,j-1)
. Wir mĂŒssen nur den Mindestwert von drei benachbarten Zellen ermitteln und einen (+1) zu diesem Wert hinzufĂŒgen, wenn die i-te Zeile und die j-te Spalte unterschiedliche Buchstaben haben.
Sie können also wieder deutlich sehen, wie rekursiv diese Aufgabe ist.

Wir haben auch gesehen, dass wir es mit einer Divide-and-Conquer-Aufgabe zu tun haben. Aber können wir dynamische Programmierung anwenden, um dieses Problem zu lösen? ErfĂŒllt diese Aufgabe die oben genannten Bedingungen fĂŒr "sich
ĂŒberschneidende Probleme " und "
optimale Unterstrukturen "?
Ja Lassen Sie uns einen Entscheidungsbaum erstellen.

ZunÀchst stellen Sie möglicherweise fest, dass unser Entscheidungsbaum eher einem
Entscheidungsdiagramm als einem
Baum Àhnelt . Möglicherweise stellen Sie auch
mehrere ĂŒberlappende Unteraufgaben fest . Es ist auch ersichtlich, dass es unmöglich ist, die Anzahl von Operationen zu reduzieren und sie kleiner als die Anzahl von Operationen aus diesen drei benachbarten Zellen zu machen (Unterprobleme).
Möglicherweise stellen Sie auch fest, dass der Wert in jeder Zelle basierend auf vorherigen Werten berechnet wird. In diesem Fall wird daher die
Tabellierungstechnik verwendet (FĂŒllen des Caches in Bottom-Up-Richtung). Sie sehen dies im folgenden Codebeispiel.
Mit all diesen Prinzipien können wir komplexere Probleme lösen, zum Beispiel die Transformationsaufgabe Samstag â Sonntag:
CodebeispielHier finden Sie eine Komplettlösung zum Ermitteln des Mindestbearbeitungsabstands mit Tests und ErklÀrungen:
function levenshteinDistance(a, b) { const distanceMatrix = Array(b.length + 1) .fill(null) .map( () => Array(a.length + 1).fill(null) ); for (let i = 0; i <= a.length; i += 1) { distanceMatrix[0][i] = i; } for (let j = 0; j <= b.length; j += 1) { distanceMatrix[j][0] = j; } for (let j = 1; j <= b.length; j += 1) { for (let i = 1; i <= a.length; i += 1) { const indicator = a[i - 1] === b[j - 1] ? 0 : 1; distanceMatrix[j][i] = Math.min( distanceMatrix[j][i - 1] + 1,
Schlussfolgerungen
In diesem Artikel haben wir zwei algorithmische AnsĂ€tze (âdynamische Programmierungâ und âTeilen und Erobernâ) mit der Lösung von Problemen verglichen. Wir haben festgestellt, dass die dynamische Programmierung das Prinzip âTeilen und Erobernâ verwendet und zur Lösung von Problemen angewendet werden kann, wenn das Problem sich ĂŒberschneidende Teilprobleme und die optimale Unterstruktur enthĂ€lt (wie dies bei der Levenshtein-Distanz der Fall ist). Die dynamische Programmierung verwendet ferner Memoisierungs- und Tabellentechniken, um Unterauflösungen fĂŒr eine spĂ€tere Wiederverwendung beizubehalten.
Ich hoffe, dieser Artikel klĂ€rt die Situation fĂŒr diejenigen unter Ihnen, die versucht haben, sich mit so wichtigen Konzepten wie dynamischer Programmierung und âTeilen und Erobernâ auseinanderzusetzen, anstatt sie zu komplizieren :)
Weitere algorithmische Beispiele mit dynamischer Programmierung mit Tests und ErklĂ€rungen finden Sie im Repository fĂŒr
JavaScript-Algorithmen und Datenstrukturen .
Erfolgreiche Codierung!