Désolé à l'avance si cette question vous semble stupide ...
Pour autant que je sache, la construction d'un algorithme utilisant la programmation dynamique fonctionne de cette façon:
- exprimer le problème comme une relation de récurrence;
- mettre en œuvre la relation de récurrence soit par mémorisation, soit par une approche ascendante.
Pour autant que je sache, j'ai tout dit sur la programmation dynamique. Je veux dire: la programmation dynamique ne donne pas d'outils / règles / méthodes / théorèmes pour exprimer des relations de récurrence, ni pour les transformer en code.
Alors, quelle est la particularité de la programmation dynamique? Qu'est-ce que cela vous apporte, à part une méthode vague pour aborder un certain type de problèmes?
Réponses:
La programmation dynamique vous permet de penser à la conception d'algorithmes. C'est souvent très utile.
La mémorisation et les méthodes ascendantes vous donnent une règle / méthode pour transformer les relations de récurrence en code. La mémorisation est une idée relativement simple, mais les meilleures idées le sont souvent!
La programmation dynamique vous donne une façon structurée de penser au temps d'exécution de votre algorithme. Le temps d'exécution est essentiellement déterminé par deux nombres: le nombre de sous-problèmes que vous devez résoudre et le temps qu'il faut pour résoudre chaque sous-problème. Cela fournit un moyen simple et pratique de réfléchir au problème de conception de l'algorithme. Lorsque vous avez une relation de récurrence candidate, vous pouvez l'examiner et obtenir très rapidement une idée de la durée d'exécution (par exemple, vous pouvez souvent dire très rapidement combien de sous-problèmes il y aura, ce qui est une limite inférieure de la temps d'exécution; s'il y a de façon exponentielle de nombreux sous-problèmes que vous devez résoudre, la récurrence ne sera probablement pas une bonne approche). Cela vous aide également à exclure les décompositions de sous-problèmes potentiels. Par exemple, si nous avons une chaîne , définissant un sous-problème par un préfixe S [ 1 .. i ] ou un suffixe S [ j . . n ] ou sous-chaîne S [ i . . j ] pourrait être raisonnable (le nombre de sous-problèmes est polynomial en n ), mais définir un sous-problème par une sous-séquence de S n'est probablement pas une bonne approche (le nombre de sous-problèmes est exponentiel en n ). Cela vous permet d'élaguer "l'espace de recherche" des récidives possibles.S[1..n] S[1..i] S[j..n] S[ i . . j ] n S n
La programmation dynamique vous offre une approche structurée pour rechercher des relations de récurrence candidates. Empiriquement, cette approche est souvent efficace. En particulier, il existe des modèles heuristiques / communs que vous pouvez reconnaître pour des manières courantes de définir des sous-problèmes, selon le type d'entrée. Par exemple:
Si l'entrée est un entier positif , une façon possible de définir un sous-problème consiste à remplacer n par un entier plus petit n ′ (st 0 ≤ n ′ ≤ n ).n n n′ 0 ≤ n′≤ n
Si l'entrée est une chaîne , certaines façons possibles de définir un sous-problème incluent: remplacer S [ 1 .. n ] par un préfixe S [ 1 .. i ] ; remplacerS[ 1 .. n ] S[ 1 .. n ] S[ 1 .. i ] par un suffixe S [ j . . n ] ; remplacer S [ 1 .. n ] par une sous-chaîne S [ i . . j ]S[ 1 .. n ] S[ j . . n ] S[ 1 .. n ] S[ i . . j ] . (Ici, le sous-problème est déterminé par le choix de .)je , j
Si l'entrée est une liste , faites la même chose que pour une chaîne.
Si l'entrée est un arbre , une façon possible de définir un sous-problème consiste à remplacer T par n'importe quel sous-arbre de T (c'est-à-dire, choisir un nœud x et remplacer T par le sous-arbre enraciné en x ; le sous-problème est déterminé par le choix de x ).T T T X T X X
Si l'entrée est une paire , examinez récursivement le type de x et le type de y pour identifier une façon de choisir un sous-problème pour chacun. En d'autres termes, une façon possible de définir un sous-problème est de remplacer ( x , y ) par ( x( x , y) X y ( x , y) où x ′ est un sous-problème pour x et y ′ est un sous-problème pour y . (Vous pouvez également considérer les sous-problèmes du formulaire ( x , y( x′, y′) X′ X y′ y Ou ( x ′ , y ) .)( x , y′) ( x′, y)
Etc. Cela vous donne une heuristique très utile: juste en regardant la signature de type de la méthode, vous pouvez trouver une liste de façons possibles de définir des sous-problèmes. En d'autres termes, juste en regardant l'énoncé du problème - en ne regardant que les types d'entrées - vous pouvez trouver une poignée de façons possibles de définir un sous-problème.
C'est souvent très utile. Il ne vous dit pas quelle est la relation de récurrence, mais lorsque vous avez un choix particulier pour définir le sous-problème, il n'est souvent pas trop difficile de trouver une relation de récurrence correspondante. Ainsi, cela transforme souvent la conception d'un algorithme de programmation dynamique en une expérience structurée. Vous écrivez sur du papier brouillon une liste de façons possibles de définir les sous-problèmes (en utilisant l'heuristique ci-dessus). Ensuite, pour chaque candidat, vous essayez d'écrire une relation de récurrence et d'évaluer son temps d'exécution en comptant le nombre de sous-problèmes et le temps passé par sous-problème. Après avoir essayé chaque candidat, vous gardez le meilleur que vous ayez pu trouver. Fournir une certaine structure au processus de conception de l'algorithme est une aide majeure, car sinon la conception de l'algorithme peut être intimidante (il '
la source
Votre compréhension de la programmation dynamique est correcte ( afaik ) et votre question est justifiée.
Je pense que l'espace de conception supplémentaire que nous obtenons du type de récurrences que nous appelons "programmation dynamique" peut être mieux vu en comparaison avec d'autres schémas d'approches récursives.
Imaginons que nos entrées soient des tableaux pour mettre en évidence les concepts.A [ 1 .. n ]
Approche inductive
Ici, l'idée est de rendre votre problème plus petit, de résoudre la version plus petite et de dériver une solution pour l'original. Schématiquement,
avec la fonction / algorithme qui traduit la solution.g
Exemple: recherche de superstars en temps linéaire
Diviser et conquérir
Partitionnez l'entrée en plusieurs parties plus petites, résolvez le problème pour chacune et combinez. Schématiquement (pour deux parties),
.F( A ) = g( f( A [ 1 .. c ] ) , f( A [ c + 1 .. n ] ) , A )
Exemples: Fusion / Tri rapide, Distance par paire la plus courte dans le plan
Programmation dynamique
Considérez toutes les façons de partitionner le problème en problèmes plus petits et choisissez le meilleur. Schématiquement (pour deux parties),
.F( A ) = meilleur{ g( f( A [ 1 .. c ] ) , f( A [ c + 1 .. n ] ) ) ∣∣1 ≤ c ≤ n - 1 }
Exemples: modification de la distance, problème de modification
Remarque importante: la programmation dynamique n'est pas une force brute ! L'application dumeilleur à chaque étape réduit considérablement l'espace de recherche.
Dans un sens, vous savez de moins en moins statiquement aller de haut en bas et devez prendre de plus en plus de décisions de manière dynamique.
La leçon de l'apprentissage de la programmation dynamique est qu'il est correct d'essayer tous les partitionnements possibles (enfin, c'est nécessaire pour l'exactitude) car il peut toujours être efficace en utilisant la mémorisation.
la source
La programmation dynamique vous permet d'échanger de la mémoire contre du temps de calcul. Prenons l'exemple classique, Fibonacci.
Fibonacci est défini par la récurrence . Si vous résolvez en utilisant cette récursivité, vous finissez par faire des appels O ( 2 n ) à F i b ( ⋅ ) , car l'arbre de récursivité est un arbre binaire de hauteur nFi b ( n ) = Fi b ( n - 1 ) + Fi b ( n - 2 ) O ( 2n) Fi b ( ⋅ ) n .
la source
Voici une autre façon légèrement différente de formuler ce que la programmation dynamique vous offre. La programmation dynamique réduit un nombre exponentiel de solutions candidates en un nombre polynomial de classes d'équivalence, de sorte que les solutions candidates dans chaque classe sont indiscernables dans un certain sens.
la source