Programmation dynamique et similitudes diviser-pour-conquérir
Comme je le vois pour le moment, je peux dire que la programmation dynamique est une extension du paradigme diviser pour conquérir .
Je ne les traiterais pas comme quelque chose de complètement différent. Parce qu'ils fonctionnent tous les deux en décomposant récursivement un problème en deux ou plusieurs sous-problèmes du même type ou d'un type apparenté, 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 d'origine.
Alors pourquoi avons-nous encore des noms de paradigme différents alors et pourquoi j'ai appelé la programmation dynamique une extension. C'est parce que l'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 étend l'approche diviser pour conquérir avec la technique de mémorisation ou de tabulation .
Allons-y étape par étape…
Conditions préalables / restrictions de programmation dynamique
Comme nous venons de le découvrir, il y a 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 des sous-problèmes - le problème peut être décomposé en sous-problèmes qui sont réutilisés plusieurs fois ou un algorithme récursif pour le problème résout le même sous-problème à plusieurs reprises plutôt que de toujours générer de nouveaux sous-problèmes
Une fois ces deux conditions remplies, nous pouvons dire que ce problème de division pour vaincre peut être résolu en utilisant une approche de programmation dynamique.
Extension de programmation dynamique pour Divide and Conquer
L'approche de programmation dynamique étend l'approche de division pour conquérir avec deux techniques ( mémorisation et tabulation ) qui ont toutes 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 O(2^n)
où la solution DP fait la même chose avec seulement le O(n)
temps.
La mémorisation (remplissage du cache de haut en bas) fait référence à la technique de mise en cache et de réutilisation des résultats précédemment calculés. La fib
fonction mémorisée 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 simple de manière itérative. La version 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émorisation et la comparaison des tableaux 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 des solutions de sous-problèmes devient possible et ainsi la mémorisation / tabulation entre en scène.
Alors, quelle est la différence entre DP et DC après tout
Puisque nous connaissons maintenant les prérequis DP et ses méthodologies, nous sommes prêts à rassembler tout ce qui a été mentionné ci-dessus en une seule image.
Si vous souhaitez voir des exemples de code, vous pouvez consulter des explications plus détaillées ici où vous trouverez deux exemples d'algorithmes: la recherche binaire et la distance minimale d'édition (distance de Levenshtein) qui illustrent la différence entre DP et DC.