Quelle est la différence entre les algorithmes Divide and Conquer et les algorithmes de programmation dynamique? En quoi les deux termes sont-ils différents? Je ne comprends pas la différence entre eux.
Veuillez prendre un exemple simple pour expliquer toute différence entre les deux et pour quel motif elles semblent similaires.
Divide and Conquer
Diviser et conquérir fonctionne en divisant le problème en sous-problèmes, en conquérant chaque sous-problème de manière récursive et en combinant ces solutions.
Programmation dynamique
La programmation dynamique est une technique permettant de résoudre les problèmes de sous-problèmes qui se chevauchent. Chaque sous-problème n'est résolu qu'une fois et le résultat de chaque sous-problème est stocké dans une table (généralement implémentée sous la forme d'un tableau ou d'une table de hachage) pour des références futures. Ces sous-solutions peuvent être utilisées pour obtenir la solution d'origine et la technique de stockage de ces solutions est connue sous le nom de mémorisation.
Vous pouvez penser à DP = recursion + re-use
Un exemple classique pour comprendre la différence serait de voir ces deux approches pour obtenir le nième nombre de fibonacci. Vérifiez ceci matériel du MIT.
Approche Divide and Conquer
Approche de programmation dynamique
L’autre différence entre diviser pour régner et programmation dynamique pourrait être:
Diviser et conquérir:
Programmation dynamique:
parfois, lorsque vous programmez de manière récursive, vous appelez la fonction avec les mêmes paramètres plusieurs fois, ce qui est inutile.
Le fameux exemple de Fibonacci:
index: 1,2,3,4,5,6...
Fibonacci number: 1,1,2,3,5,8...
function F(n) {
if (n < 3)
return 1
else
return F(n-1) + F(n-2)
}
Courons F (5):
F(5) = F(4) + F(3)
= {F(3)+F(2)} + {F(2)+F(1)}
= {[F(2)+F(1)]+1} + {1+1}
= 1+1+1+1+1
Nous avons donc appelé: 1 fois F(4) 2 fois F(3) 3 fois F(2) 2 fois F (1)
Approche de programmation dynamique: si vous appelez une fonction avec le même paramètre plusieurs fois, enregistrez le résultat dans une variable pour y accéder directement lors de la prochaine fois. La manière itérative:
if (n==1 || n==2)
return 1
else
f1=1, f2=1
for i=3 to n
f = f1 + f2
f1 = f2
f2 = f
Appelons F(5) encore:
fibo1 = 1
fibo2 = 1
fibo3 = (fibo1 + fibo2) = 1 + 1 = 2
fibo4 = (fibo2 + fibo3) = 1 + 2 = 3
fibo5 = (fibo3 + fibo4) = 2 + 3 = 5
Comme vous pouvez le constater, chaque fois que vous avez besoin d'appels multiples, vous n'avez qu'à accéder à la variable correspondante pour obtenir la valeur au lieu de la recalculer.
À propos, la programmation dynamique ne signifie pas convertir un code récursif en un code itératif. Vous pouvez également enregistrer les sous-résultats dans une variable si vous souhaitez un code récursif. Dans ce cas, la technique s'appelle la mémorisation. Pour notre exemple, il ressemble à ceci:
// declare and initialize a dictionary
var dict = new Dictionary<int,int>();
for i=1 to n
dict[i] = -1
function F(n) {
if (n < 3)
return 1
else
{
if (dict[n] == -1)
dict[n] = F(n-1) + F(n-2)
return dict[n]
}
}
Donc, la relation avec Divide and Conquer est que les algorithmes de D & D reposent sur la récursion. Et certaines versions d’entre elles ont cet "appel à plusieurs fonctions avec le même problème de paramètre". Recherchez "multiplication de chaîne matricielle" et "sous-séquence la plus longue commune" pour de tels exemples où DP est nécessaire pour améliorer le T(n) de D & D algo.
Comme je le vois pour le moment, je peux dire que la programmation dynamique est une extension du paradigme de diviser pour régner .
Je ne les traiterais pas comme quelque chose de complètement différent. Parce que , ils travaillent tous les deux en décomposant récursivement un problème en deux sous-problèmes ou plus du même type ou d'un type lié, jusqu'à ce que ceux-ci deviennent suffisamment simples pour être résolus directement. Les solutions aux sous-problèmes sont ensuite combinées pour donner une solution au problème initial.
Alors pourquoi avons-nous toujours des noms de paradigmes différents alors et pourquoi j’ai appelé la programmation dynamique une extension. En effet, une approche de programmation dynamique peut être appliquée au problème uniquement si le problème comporte certaines restrictions ou conditions préalables . Et après cette programmation dynamique, élargit l’approche diviser pour mieux régner avec mémoization ou tabulation technique.
Allons-y pas à pas…
Comme nous venons de le découvrir, il existe deux attributs clés que le problème de division et de conquête doit avoir pour que la programmation dynamique soit applicable:
Sous-structure optimale - une solution optimale peut être construite à partir de solutions optimales de ses sous-problèmes
Chevauchement de sous-problèmes - le problème peut être divisé en sous-problèmes réutilisés plusieurs fois ou un algorithme récursif du problème résout le même sous-problème plusieurs fois. que toujours générer de nouveaux sous-problèmes
Une fois que ces deux conditions sont remplies, nous pouvons dire que ce problème de division et de conquête peut être résolu en utilisant une approche de programmation dynamique.
L’approche de programmation dynamique étend l’approche diviser pour régner avec deux techniques ( la mémoisation et la tabulation ) qui ont tous deux pour but de stocker et de réutiliser des solutions de sous-problèmes susceptibles d’améliorer considérablement les performances. Par exemple, l'implémentation récursive naïve de la fonction de Fibonacci a une complexité temporelle de O(2^n)
, où la solution DP effectue la même chose avec seulement O(n)
temps.
La mémorisation (remplissage du cache descendant) fait référence à la technique de mise en cache et de réutilisation des résultats précédemment calculés. La fonction mémoisée fib
ressemblerait donc à ceci:
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]
}
La tabulation (remplissage du cache de bas en haut) est similaire mais se concentre sur le remplissage des entrées du cache. Le calcul des valeurs dans le cache est plus facile à effectuer de manière itérative. La version de tabulation de fib
ressemblerait à ceci:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Vous pouvez en savoir plus sur la mémoisation et la comparaison de tabulation ici .
L'idée principale que vous devez comprendre ici est que parce que notre problème de division et de conquête a des sous-problèmes qui se chevauchent, la mise en cache de solutions de sous-problèmes devient possible et donc la mémorisation/la tabulation s'intensifie sur la scène.
Étant donné que nous connaissons maintenant les conditions préalables et les méthodologies de DP, nous sommes prêts à mettre en une seule photo tout ce qui a été mentionné ci-dessus.
Si vous voulez voir des exemples de code, vous pouvez consulter explication plus détaillée ici où vous trouverez deux exemples d'algorithmes: Recherche binaire et Distance minimale d'édition (Distance de Levenshtein) illustrant la différence entre les points de vente. et DC.
Je suppose que vous avez déjà lu Wikipedia et d’autres ressources académiques à ce sujet. Je ne vais donc pas recycler ces informations. Je dois également préciser que je ne suis en aucun cas un expert en informatique, mais je partagerai mes deux cents sur ma compréhension de ces sujets ...
Décompose le problème en sous-problèmes distincts. L'algorithme récursif de la séquence de Fibonacci est un exemple de programmation dynamique car il résout pour fib (n) en résolvant d'abord pour fib (n-1). Afin de résoudre le problème initial, il résout un problème différent.
Ces algorithmes résolvent généralement des parties similaires du problème, puis les rassemblent à la fin. Mergesort est un exemple classique de division et de conquête. La principale différence entre cet exemple et l'exemple de Fibonacci réside dans le fait que, dans un mergesort, la division peut (théoriquement) être arbitraire et que, quelle que soit la manière dont vous la découpez, vous continuez à fusionner et à trier. La même quantité de travail doit être effectuée pour fusionner le tableau, quelle que soit la manière dont vous le divisez. Résoudre pour fib (52) nécessite plus d’étapes que résoudre pour fib (2).
Je pense que Divide & Conquer
Est une approche récursive et que Dynamic Programming
Est un remplissage de table.
Par exemple, Merge Sort
Est un algorithme Divide & Conquer
. Dans chaque étape, vous divisez le tableau en deux moitiés, appelez récursivement Merge Sort
Sur les deux moitiés, puis vous les fusionnez.
Knapsack
est un algorithme Dynamic Programming
, car vous remplissez un tableau représentant des solutions optimales aux sous-problèmes de l'ensemble du sac à dos. Chaque entrée dans le tableau correspond à la valeur maximale que vous pouvez emporter dans un sac de poids w les éléments 1-j donnés.