rev4 : Un commentaire très éloquent de l'utilisateur Sammaron a noté que, peut-être, cette réponse a précédemment confondu top-down et bottom-up. Alors qu'à l'origine cette réponse (rev3) et d'autres réponses disaient que "bottom-up est la mémorisation" ("assumer les sous-problèmes"), c'est peut-être l'inverse (c'est-à-dire que "top-down" peut être "assumer les sous-problèmes" et "bottom-up" peut être "composer les sous-problèmes"). Auparavant, j'ai lu que la mémorisation était un type différent de programmation dynamique, par opposition à un sous-type de programmation dynamique. Je citais ce point de vue bien que n'y adhérant pas. J'ai réécrit cette réponse pour être agnostique sur la terminologie jusqu'à ce que des références appropriées puissent être trouvées dans la littérature. J'ai également converti cette réponse en un wiki communautaire. Veuillez préférer les sources académiques. Liste de références : {Web : 1 , 2 } {Littérature : 5 }
Récapitulatif
La programmation dynamique consiste à ordonner vos calculs de manière à éviter de recalculer le travail en double. Vous avez un problème principal (la racine de votre arbre de sous-problèmes), et des sous-problèmes (sous-arbres). Les sous-problèmes se répètent et se chevauchent généralement. .
Par exemple, considérez votre exemple préféré de Fibonnaci. Voici l'arbre complet des sous-problèmes, si nous faisions un appel récursif naïf :
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
(Dans certains autres problèmes rares, cet arbre pourrait être infini dans certaines branches, représentant la non-termination, et donc le bas de l'arbre peut être infiniment grand. En outre, dans certains problèmes, il se peut que vous ne sachiez pas à l'avance à quoi ressemble l'arbre complet. Ainsi, vous pourriez avoir besoin d'une stratégie/algorithme pour décider des sous-problèmes à révéler).
Mémorisation, tabulation
Il existe au moins deux techniques principales de programmation dynamique, qui ne s'excluent pas mutuellement :
-
Mémorisation - Il s'agit d'une approche de laissez-faire : Vous supposez que vous avez déjà calculé tous les sous-problèmes et que vous n'avez aucune idée de l'ordre d'évaluation optimal. Typiquement, vous effectuez un appel récursif (ou un équivalent itératif) à partir de la racine, et vous espérez soit vous rapprocher de l'ordre d'évaluation optimal, soit obtenir une preuve qui vous aidera à arriver à l'ordre d'évaluation optimal. Vous vous assurerez que l'appel récursif ne recompile jamais un sous-problème parce que vous cache les résultats, et donc les sous-arbres dupliqués ne sont pas recalculés.
-
ejemplo: Si vous calculez la séquence de Fibonacci
fib(100)
tu appellerais juste ça, et ça appellerait fib(100)=fib(99)+fib(98)
qui appellerait fib(99)=fib(98)+fib(97)
, ...etc..., qui appellerait fib(2)=fib(1)+fib(0)=1+0=1
. Ensuite, il serait enfin résolu fib(3)=fib(2)+fib(1)
mais il n'a pas besoin de recalculer. fib(2)
parce que nous l'avons mis en cache.
- Cela commence au sommet de l'arbre et évalue les sous-problèmes des feuilles/sous-arbres en remontant vers la racine.
-
Tabulation - Vous pouvez également considérer la programmation dynamique comme un algorithme de "remplissage de table" (bien que généralement multidimensionnelle, cette "table" peut avoir une géométrie non euclidienne dans de très rares cas*). C'est comme la mémorisation mais plus actif, et implique une étape supplémentaire : Vous devez choisir, à l'avance, l'ordre exact dans lequel vous allez effectuer vos calculs. Cela ne signifie pas que l'ordre doit être statique, mais que vous avez beaucoup plus de flexibilité que la mémorisation.
-
ejemplo: Si vous faites du fibonacci, vous pouvez choisir de calculer les nombres dans cet ordre :
fib(2)
, fib(3)
, fib(4)
... mettre en cache chaque valeur afin de pouvoir calculer les suivantes plus facilement. Vous pouvez également considérer que cela revient à remplir une table (une autre forme de mise en cache).
- Personnellement, je n'entends pas souvent le mot "tabulation", mais c'est un terme très correct. Certaines personnes considèrent cela comme de la "programmation dynamique".
- Avant d'exécuter l'algorithme, le programmeur considère l'arbre entier, puis écrit un algorithme pour évaluer les sous-problèmes dans un ordre particulier vers la Racine, en remplissant généralement un tableau.
- *Note de bas de page : Parfois, le "tableau" n'est pas un tableau rectangulaire avec une connectivité de type grille, en soi. Elle peut plutôt avoir une structure plus complexe, comme un arbre, ou une structure spécifique au domaine du problème (par exemple, les villes situées à une distance de vol sur une carte), ou même un diagramme en treillis, qui, bien que semblable à une grille, n'a pas une structure de connectivité haut-bas-gauche-droite, etc. Par exemple, l'utilisateur 3290797 a lié un exemple de programmation dynamique visant à trouver le ensemble indépendant maximal dans un arbre ce qui correspond à remplir les cases vides d'un arbre.
(Dans sa forme la plus générale, dans un paradigme de "programmation dynamique", je dirais que le programmeur considère l'arbre entier, puis écrit un algorithme qui met en œuvre une stratégie d'évaluation des sous-problèmes qui peut optimiser les propriétés que vous souhaitez (généralement une combinaison de complexité temporelle et spatiale). Votre stratégie doit commencer quelque part, avec un sous-problème particulier, et peut éventuellement s'adapter en fonction des résultats de ces évaluations. Dans le sens général de la "programmation dynamique", vous pourriez essayer de mettre en cache ces sous-problèmes, et plus généralement, essayer d'éviter de revisiter les sous-problèmes, une distinction subtile étant peut-être le cas des graphes dans diverses structures de données. Très souvent, ces structures de données sont à la base comme des tableaux ou des tables. Les solutions aux sous-problèmes peuvent être jetées si nous n'en avons plus besoin).
[Il existe clairement deux approches principales appelées Mémorisation et Tabulation qui peuvent être en bijection avec ces termes (mais pas entièrement). Le terme général que la plupart des gens utilisent reste "Programmation dynamique" et certains parlent de "mémorisation" pour désigner ce sous-type particulier de "Programmation dynamique". Cette réponse refuse de dire ce qui est descendant et ascendant jusqu'à ce que la communauté puisse trouver les références appropriées dans des articles universitaires. En définitive, il est important de comprendre la distinction plutôt que la terminologie].
Avantages et inconvénients
Facilité de codage
La mémorisation est très facile à coder (vous pouvez généralement* écrire une annotation "memoizer" ou une fonction wrapper qui le fait automatiquement pour vous), et devrait être votre première ligne d'approche. L'inconvénient de la tabulation est que vous devez trouver un ordre.
*(en fait, cela n'est facile que si vous écrivez la fonction vous-même, et/ou si vous codez dans un langage de programmation impur/non fonctionnel... par exemple si quelqu'un a déjà écrit une fonction précompilée fib
elle fait nécessairement des appels récursifs à elle-même, et vous ne pouvez pas magiquement mémoriser la fonction sans vous assurer que ces appels récursifs appellent votre nouvelle fonction mémorisée (et non la fonction originale non mémorisée)).
Récursivité
Notez que les deux méthodes, descendante et ascendante, peuvent être implémentées avec une récursion ou un remplissage itératif du tableau, bien que cela ne soit pas naturel.
Préoccupations pratiques
Avec la mémoïsation, si l'arbre est très profond (par ex. fib(10^6)
), vous manquerez d'espace sur la pile, car chaque calcul retardé doit être mis sur la pile, et vous en aurez 10^6.
Optimalité
L'une ou l'autre approche peut ne pas être optimale en termes de temps si l'ordre dans lequel vous visitez (ou essayez de visiter) les sous-problèmes n'est pas optimal, en particulier s'il existe plus d'une façon de calculer un sous-problème (normalement, la mise en cache devrait résoudre ce problème, mais il est théoriquement possible que la mise en cache ne le fasse pas dans certains cas exotiques). La mémorisation ajoutera généralement à votre complexité temporelle une complexité spatiale (par exemple, avec la tabulation, vous avez plus de liberté pour jeter des calculs, comme l'utilisation de la tabulation avec Fib vous permet d'utiliser O(1) d'espace, mais la mémorisation avec Fib utilise O(N) d'espace de pile).
Optimisations avancées
Si vous faites aussi des problèmes extrêmement compliqués, vous n'aurez peut-être pas d'autre choix que de faire de la tabulation (ou au moins de jouer un rôle plus actif en dirigeant la mémorisation là où vous voulez qu'elle aille). De même, si vous êtes dans une situation où l'optimisation est absolument critique et que vous devez optimiser, la tabulation vous permettra de faire des optimisations que la mémoïsation ne vous permettrait pas de faire de manière sensée. À mon humble avis, dans le génie logiciel normal, aucun de ces deux cas ne se présente jamais, donc j'utiliserais simplement la mémorisation ("une fonction qui met en cache ses réponses") à moins que quelque chose (comme l'espace de la pile) rende la tabulation nécessaire... bien que techniquement pour éviter une explosion de la pile vous pouvez 1) augmenter la taille limite de la pile dans les langages qui le permettent, ou 2) manger un facteur constant de travail supplémentaire pour virtualiser votre pile (ick), ou 3) programmer dans le style continuation-passing, qui en effet virtualise également votre pile (je ne suis pas sûr de la complexité de ceci, mais fondamentalement vous allez effectivement prendre la chaîne d'appel différé de la pile de taille N et de-facto la coller dans N fonctions thunk successivement imbriquées. ... bien que dans certains langages sans optimisation des appels de queue, vous pouvez avoir à trampiler les choses pour éviter une explosion de la pile).
Des exemples plus compliqués
Nous énumérons ici des exemples d'intérêt particulier, qui ne sont pas seulement des problèmes généraux de DP, mais qui distinguent de manière intéressante la mémorisation et la tabulation. Par exemple, une formulation peut être beaucoup plus facile que l'autre, ou il peut y avoir une optimisation qui nécessite essentiellement la tabulation :
- l'algorithme pour calculer l'edit-distance[ 4 ], intéressant en tant qu'exemple non trivial d'un algorithme de remplissage de tableaux à deux dimensions
6 votes
En rapport : stackoverflow.com/questions/6184869/