rev4: Un commentaire très éloquent de l'utilisateur Sammaron a noté que, peut-être, cette réponse confondait auparavant le haut vers le bas et le bas vers le haut. Alors qu'à l'origine cette réponse (rev3) et d'autres réponses disaient que "de bas en haut est la mémorisation" ("assumer les sous-problèmes"), cela peut être l'inverse (c'est-à-dire que "de haut en bas" peut être "assumer les sous-problèmes" et " de bas en haut "peut être" composer les sous-problèmes "). Auparavant, j'avais 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 je n'y souscris pas. J'ai réécrit cette réponse pour être indépendante de 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:} {Littérature: 5 }
résumer
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 arborescence de sous-problèmes), et des sous-problèmes (sous-arborescences). 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 avons fait 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 peut être infini dans certaines branches, ce qui représente la non-terminaison, et ainsi le bas de l'arbre peut être infiniment grand. De plus, dans certains problèmes, vous pourriez ne pas savoir à quoi ressemble l'arbre complet avant Vous aurez peut-être besoin d’une stratégie / d’un 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 sont pas mutuellement exclusives:
Mémorisation - Il s'agit d'une approche de laisser-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. En règle générale, vous effectuez un appel récursif (ou un équivalent itératif) à partir de la racine, et espérez que vous vous rapprocherez de l'ordre d'évaluation optimal, ou obtenez une preuve que vous vous aiderez à arriver à l'ordre d'évaluation optimal. Vous vous assurez que l'appel récursif ne recalcule jamais un sous-problème car vous mettez en cache les résultats et ainsi les sous-arborescences dupliquées ne sont pas recalculées.
- exemple: Si vous calculez la séquence de Fibonacci
fib(100)
, vous appelleriez simplement ceci, et il 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 finalement résolu fib(3)=fib(2)+fib(1)
, mais il n'a pas besoin de recalculer fib(2)
, car nous l'avons mis en cache.
- Cela commence au sommet de l'arborescence et évalue les sous-problèmes des feuilles / sous-arbres vers la racine.
Tabulation - Vous pouvez également considérer la programmation dynamique comme un algorithme de «remplissage de table» (bien que généralement multidimensionnel, 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 ferez vos calculs. Cela ne doit pas impliquer que l'ordre doit être statique, mais que vous avez beaucoup plus de flexibilité que la mémorisation.
- exemple: Si vous effectuez fibonacci, vous pouvez choisir de calculer les chiffres dans cet ordre:
fib(2)
, fib(3)
, fib(4)
... la mise en cache toutes les valeurs pour que vous puissiez calculer les prochains plus facilement. Vous pouvez également penser que cela remplit une table (une autre forme de mise en cache).
- Personnellement, je n'entends pas beaucoup le mot «tabulation», mais c'est un terme très décent. Certaines personnes considèrent cette "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é en forme de grille, en soi. Au contraire, il peut avoir une structure plus compliquée, comme un arbre, ou une structure spécifique au domaine du problème (par exemple des villes à 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, user3290797 a lié un exemple de programmation dynamique de recherche de l' ensemble indépendant maximum dans une arborescence , ce qui correspond à remplir les espaces dans une arborescence.
(Au plus général, dans un paradigme de "programmation dynamique", je dirais que le programmeur considère l'ensemble de l'arbre, alorsécrit un algorithme qui implémente une stratégie pour évaluer les sous-problèmes qui peuvent optimiser les propriétés que vous voulez (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-être s’adapter en fonction des résultats de ces évaluations. Dans le sens général de "programmation dynamique", vous pouvez essayer de mettre en cache ces sous-problèmes, et plus généralement, éviter de revoir les sous-problèmes avec une distinction subtile pouvant être le cas des graphiques dans diverses structures de données. Très souvent, ces structures de données sont à leur base comme des tableaux ou des tableaux. Les solutions aux sous-problèmes peuvent être jetées si nous n'en avons plus besoin.)
[Auparavant, cette réponse faisait une déclaration sur la terminologie descendante et ascendante; 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 est toujours «programmation dynamique» et certaines personnes disent «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 les articles universitaires. En fin de compte, 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 "mémo" 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 établir une commande.
* (ce n'est en fait facile que si vous écrivez la fonction vous-même, et / ou codez dans un langage de programmation impur / non fonctionnel ... par exemple si quelqu'un a déjà écrit une fib
fonction précompilée , il effectue nécessairement des appels récursifs à lui-même, et vous ne pouvez pas mémoriser la fonction par magie sans vous assurer que ces appels récursifs appellent votre nouvelle fonction mémorisée (et non la fonction non mémorisée d'origine))
Récursivité
Notez que le haut et le bas peuvent être implémentés avec une récursivité ou un remplissage de table itératif, bien que cela ne soit pas naturel.
Préoccupations pratiques
Avec la mémorisation, si l'arbre est très profond (par exemple fib(10^6)
), vous manquerez d'espace de 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 dans le temps si l'ordre dans lequel vous passez (ou essayez) de visiter les sous-problèmes n'est pas optimal, en particulier s'il existe plusieurs façons de calculer un sous-problème (normalement, la mise en cache résoudrait cela, mais il est théoriquement possible que la mise en cache puisse pas dans certains cas exotiques). La mémorisation ajoutera généralement de votre complexité temporelle à votre complexité spatiale (par exemple, avec la tabulation, vous avez plus de liberté pour rejeter les calculs, comme l'utilisation de la tabulation avec Fib vous permet d'utiliser l'espace O (1), mais la mémorisation avec Fib utilise O (N) espace de pile).
Optimisations avancées
Si vous faites également un problème extrêmement compliqué, vous n'aurez peut-être pas d'autre choix que de faire une tabulation (ou du moins de jouer un rôle plus actif en dirigeant la mémorisation là où vous voulez qu'elle aille). De plus, 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émorisation ne vous permettrait pas autrement de faire de manière saine. À mon humble avis, dans l'ingénierie logicielle normale, 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 pile) ne rende la tabulation nécessaire ... techniquement pour éviter une explosion de pile, vous pouvez 1) augmenter la limite de taille de pile dans les langues qui le permettent, ou 2) consommer un facteur constant de travail supplémentaire pour virtualiser votre pile (ick),
Exemples plus compliqués
Nous énumérons ici des exemples présentant un intérêt particulier, qui ne sont pas seulement des problèmes généraux de DP, mais 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 une tabulation:
- l'algorithme de calcul de la distance d'édition [ 4 ], intéressant comme exemple non trivial d'algorithme de remplissage de table bidimensionnel