Qu'est-ce que la programmation dynamique? [fermé]

277

Qu'est-ce que la programmation dynamique ?

En quoi est-ce différent de la récursivité, de la mémorisation, etc.?

J'ai lu l' article wikipedia à ce sujet, mais je ne le comprends toujours pas vraiment.

smac89
la source
1
Voici un tutoriel de Michael A. Trick de CMU que j'ai trouvé particulièrement utile: mat.gsia.cmu.edu/classes/dynamic/dynamic.html C'est certainement en plus de toutes les ressources que d'autres ont recommandées (toutes les autres ressources, spécialement CLR et Kleinberg, Tardos sont très bons!). La raison pour laquelle j'aime ce tutoriel est qu'il introduit les concepts avancés assez progressivement. C'est du matériel un peu vieillot mais c'est un bon ajout à la liste des ressources présentées ici. Consultez également la page et les conférences de Steven Skiena sur la programmation dynamique: cs.sunysb.edu/~algorith/video-lectures http:
Edmon
11
J'ai toujours trouvé que "programmation dynamique" était un terme déroutant - "dynamique" suggère non statique, mais qu'est-ce que "programmation statique"? Et "... Programmation" évoque "Programmation Orientée Objet" et "Programmation Fonctionnelle", suggérant que DP est un paradigme de programmation. Je n'ai pas vraiment de meilleur nom (peut-être "Dynamic Algorithms"?) Mais c'est dommage que nous soyons coincés avec celui-ci.
dimo414
3
@ dimo414 La "programmation" ici est davantage liée à la "programmation linéaire" qui relève d'une classe de méthodes d'optimisation mathématique. Voir l'article Optimisation mathématique pour une liste d'autres méthodes de programmation mathématique.
syockit
1
@ dimo414 La "programmation" dans ce contexte fait référence à une méthode tabulaire, pas à l'écriture de code informatique. - Coreman
user2618142
Le problème de minimisation du coût des billets de bus décrit dans cs.stackexchange.com/questions/59797/… est mieux résolu dans la programmation dynamique.
truthadjustr

Réponses:

211

La programmation dynamique consiste à utiliser les connaissances passées pour faciliter la résolution d'un problème futur.

Un bon exemple est la résolution de la séquence de Fibonacci pour n = 1 000 002.

Ce sera un processus très long, mais que se passe-t-il si je vous donne les résultats pour n = 1 000 000 et n = 1 000 001? Soudain, le problème est devenu plus gérable.

La programmation dynamique est beaucoup utilisée dans les problèmes de chaîne, tels que le problème d'édition de chaîne. Vous résolvez un sous-ensemble du problème, puis utilisez ces informations pour résoudre le problème d'origine le plus difficile.

Avec la programmation dynamique, vous stockez généralement vos résultats dans une sorte de tableau. Lorsque vous avez besoin de la réponse à un problème, vous référencez le tableau et voyez si vous savez déjà de quoi il s'agit. Sinon, vous utilisez les données de votre tableau pour vous donner un tremplin vers la réponse.

Le livre Cormen Algorithms contient un excellent chapitre sur la programmation dynamique. ET c'est gratuit sur Google Livres! Découvrez-le ici.

samoz
la source
50
Mais ne venez-vous pas de décrire la mémorisation?
dreadwail
31
Je dirais que la mémorisation est une forme de programmation dynamique, lorsque la fonction / méthode mémorisée est récursive.
Daniel Huckstep
6
Bonne réponse, ne ferait qu'ajouter une mention sur la sous-structure optimale (par exemple, chaque sous-ensemble de tout chemin le long du chemin le plus court de A à B est lui-même le chemin le plus court entre les 2 points d'extrémité en supposant une métrique de distance qui observe l'inégalité du triangle).
Shea
5
Je ne dirais pas "plus facile", mais plus rapide. Un malentendu courant est que dp résout des problèmes que les algorithmes naïfs ne peuvent pas et ce n'est pas le cas. Ce n'est pas une question de fonctionnalité mais de performance.
andandandand
6
En utilisant la mémorisation, les problèmes de programmation dynamique peuvent être résolus de manière descendante. c'est-à-dire appeler la fonction pour calculer la valeur finale, et cette fonction à son tour l'appelle de façon récursive pour résoudre les sous-problèmes. Sans cela, les problèmes de programmation dynamique ne peuvent être résolus que de façon ascendante.
Pranav
176

La programmation dynamique est une technique utilisée pour éviter de calculer plusieurs fois le même sous-problème dans un algorithme récursif.

Prenons l'exemple simple des nombres de Fibonacci: trouver le n ème nombre de Fibonacci défini par

F n = F n-1 + F n-2 et F 0 = 0, F 1 = 1

Récursivité

La façon évidente de le faire est récursive:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

Programmation dynamique

  • Top Down - Mémorisation

La récursivité fait beaucoup de calculs inutiles car un nombre de Fibonacci donné sera calculé plusieurs fois. Un moyen simple d'améliorer cela est de mettre en cache les résultats:

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]
  • De bas en haut

Une meilleure façon de le faire est de se débarrasser de la récursivité en évaluant les résultats dans le bon ordre:

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

Nous pouvons même utiliser un espace constant et ne stocker que les résultats partiels nécessaires en cours de route:

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi
  • Comment appliquer la programmation dynamique?

    1. Trouvez la récursivité dans le problème.
    2. De haut en bas: stockez la réponse pour chaque sous-problème dans un tableau pour éviter d'avoir à les recalculer.
    3. De bas en haut: trouvez le bon ordre pour évaluer les résultats afin que des résultats partiels soient disponibles en cas de besoin.

La programmation dynamique fonctionne généralement pour les problèmes qui ont un ordre inhérent de gauche à droite tels que les chaînes, les arbres ou les séquences entières. Si l'algorithme récursif naïf ne calcule pas le même sous-problème plusieurs fois, la programmation dynamique n'aidera pas.

J'ai fait une collection de problèmes pour aider à comprendre la logique: https://github.com/tristanguigue/dynamic-programing

Tristan
la source
3
C'est une excellente réponse et la collecte de problèmes sur Github est également très utile. Merci!
p4sh4
Juste par curiosité pour clarifier les choses - à votre avis, une implémentation récursive utilisant une relation de récurrence et la mémorisation est une programmation dynamique?
Codor
Merci pour l'explication. Y a-t-il une condition manquante de bas en haut: if n in cachecomme avec l'exemple de haut en bas ou est-ce que je manque quelque chose.
DavidC
Dois-je bien comprendre que toute boucle où les valeurs calculées à chaque itération sont utilisées dans les itérations suivantes est un exemple de programmation dynamique?
Alexey
Pourriez-vous donner des références pour l'interprétation que vous avez donnée, y compris les cas spéciaux descendants et ascendants?
Alexey
38

La mémorisation est le moment où vous stockez les résultats précédents d'un appel de fonction (une fonction réelle renvoie toujours la même chose, étant donné les mêmes entrées). Cela ne fait aucune différence pour la complexité algorithmique avant le stockage des résultats.

La récursivité est la méthode d'une fonction qui s'appelle elle-même, généralement avec un ensemble de données plus petit. Comme la plupart des fonctions récursives peuvent être converties en fonctions itératives similaires, cela ne fait pas non plus de différence pour la complexité algorithmique.

La programmation dynamique est le processus de résolution de sous-problèmes plus faciles à résoudre et de construction de la réponse à partir de cela. La plupart des algorithmes DP seront dans les temps d'exécution entre un algorithme gourmand (s'il en existe un) et un algorithme exponentiel (énumérer toutes les possibilités et trouver la meilleure).

  • Les algorithmes DP peuvent être implémentés avec récursivité, mais ils ne doivent pas l'être.
  • Les algorithmes DP ne peuvent pas être accélérés par mémorisation, car chaque sous-problème n'est résolu qu'une seule fois (ou la fonction "résoudre" est appelée).
philomathohollic
la source
Très clairement. Je souhaite que les instructeurs d'algorithmes puissent bien expliquer cela.
Kelly S. French
22

C'est une optimisation de votre algorithme qui réduit le temps d'exécution.

Alors qu'un algorithme gourmand est généralement appelé naïf , car il peut s'exécuter plusieurs fois sur le même ensemble de données, la programmation dynamique évite cet écueil grâce à une compréhension plus approfondie des résultats partiels qui doivent être stockés pour aider à construire la solution finale.

Un exemple simple est de parcourir un arbre ou un graphique uniquement à travers les nœuds qui contribueraient à la solution, ou de mettre dans un tableau les solutions que vous avez trouvées jusqu'à présent afin d'éviter de parcourir les mêmes nœuds encore et encore.

Voici un exemple d'un problème adapté à la programmation dynamique, tiré du juge en ligne d'UVA: Edit Steps Ladder.

Je vais faire un bref exposé de la partie importante de l'analyse de ce problème, tirée du livre Programming Challenges, je vous suggère de le vérifier.

Jetez un bon coup d'oeil à ce problème, si nous définissons une fonction de coût nous indiquant la distance entre deux chaînes, nous en avons deux à considérer les trois types naturels de changements:

Substitution - changez un seul caractère du motif "s" en un caractère différent dans le texte "t", comme changer "coup" en "point".

Insertion - insérez un seul caractère dans le motif «s» pour l'aider à faire correspondre le texte «t», par exemple en remplaçant «il y a» par «agog».

Suppression - supprimez un seul caractère du motif "s" pour l'aider à faire correspondre le texte "t", par exemple en remplaçant "heure" par "notre".

Lorsque nous définissons chacune de ces opérations pour coûter une étape, nous définissons la distance d'édition entre deux chaînes. Alors, comment pouvons-nous le calculer?

Nous pouvons définir un algorithme récursif en observant que le dernier caractère de la chaîne doit être mis en correspondance, substitué, inséré ou supprimé. Couper les caractères dans la dernière opération d'édition laisse une opération de paire laisse une paire de chaînes plus petites. Soit i et j le dernier caractère du préfixe pertinent de et t, respectivement. il y a trois paires de chaînes plus courtes après la dernière opération, correspondant à la chaîne après une correspondance / substitution, une insertion ou une suppression. Si nous connaissions le coût de l'édition des trois paires de chaînes plus petites, nous pourrions décider quelle option conduit à la meilleure solution et choisir cette option en conséquence. Nous pouvons apprendre ce coût, grâce à la chose impressionnante qu'est la récursivité:

#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */


int string_compare(char *s, char *t, int i, int j)

{

    int k; /* counter */
    int opt[3]; /* cost of the three options */
    int lowest_cost; /* lowest cost */
    if (i == 0) return(j * indel(’ ’));
    if (j == 0) return(i * indel(’ ’));
    opt[MATCH] = string_compare(s,t,i-1,j-1) +
      match(s[i],t[j]);
    opt[INSERT] = string_compare(s,t,i,j-1) +
      indel(t[j]);
    opt[DELETE] = string_compare(s,t,i-1,j) +
      indel(s[i]);
    lowest_cost = opt[MATCH];
    for (k=INSERT; k<=DELETE; k++)
    if (opt[k] < lowest_cost) lowest_cost = opt[k];
    return( lowest_cost );

}

Cet algorithme est correct, mais il est également incroyablement lent.

Fonctionnant sur notre ordinateur, il faut plusieurs secondes pour comparer deux chaînes de 11 caractères, et le calcul disparaît pour ne plus jamais atterrir sur quoi que ce soit.

Pourquoi l'algorithme est-il si lent? Cela prend du temps exponentiel car il recalcule les valeurs encore et encore et encore. À chaque position de la chaîne, la récursivité se ramifie de trois façons, ce qui signifie qu'elle croît à un rythme d'au moins 3 ^ n - en fait, encore plus rapidement car la plupart des appels ne réduisent qu'un seul des deux indices, pas les deux.

Alors, comment pouvons-nous rendre l'algorithme pratique? L'observation importante est que la plupart de ces appels récursifs calculent des choses qui ont déjà été calculées auparavant. Comment savons nous? Eh bien, il ne peut y avoir que | s | · | T | possibles appels récursifs uniques, car il n'y a que autant de paires distinctes (i, j) qui serviront de paramètres d'appels récursifs.

En stockant les valeurs de chacune de ces (i, j) paires dans une table, nous pouvons éviter de les recalculer et simplement les rechercher selon les besoins.

Le tableau est une matrice bidimensionnelle m où chacun des | s | · | t | cells contient le coût de la solution optimale de ce sous-problème, ainsi qu'un pointeur parent expliquant comment nous sommes arrivés à cet emplacement:

typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;

cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */

La version de programmation dynamique présente trois différences par rapport à la version récursive.

Première, il obtient ses valeurs intermédiaires en utilisant la recherche de table au lieu d'appels récursifs.

** Deuxièmement, ** il met à jour le champ parent de chaque cellule, ce qui nous permettra de reconstruire la séquence d'édition plus tard.

** Troisièmement, ** Troisièmement, il est instrumenté à l'aide d'une cell()fonction d' objectif plus générale au lieu de simplement renvoyer m [| s |] [| t |] .cost. Cela nous permettra d'appliquer cette routine à une classe plus large de problèmes.

Ici, une analyse très particulière de ce qu'il faut pour obtenir les résultats partiels les plus optimaux est ce qui fait de la solution une solution «dynamique».

Voici une solution alternative et complète au même problème. Il est également "dynamique" même si son exécution est différente. Je vous suggère de vérifier l'efficacité de la solution en la soumettant au juge en ligne d'UVA. Je trouve étonnant de voir comment un problème aussi lourd a été abordé si efficacement.

andandandand
la source
Le stockage est-il vraiment nécessaire pour être une programmation dynamique? Est-ce qu'une quantité de travail à sauter ne qualifierait pas un algorithme de dynamique?
Nthalk
Vous devez recueillir des résultats optimaux étape par étape pour rendre un algorithme "dynamique". La programmation dynamique découle du travail de Bellman en salle d'opération, si vous dites "que sauter n'importe quelle quantité de mot est une programmation dynamique", vous dévalorisez le terme, car toute heuristique de recherche serait une programmation dynamique. en.wikipedia.org/wiki/Dynamic_programming
andandandand
13

Les bits clés de la programmation dynamique sont les "sous-problèmes qui se chevauchent" et la "sous-structure optimale". Ces propriétés d'un problème signifient qu'une solution optimale est composée des solutions optimales à ses sous-problèmes. Par exemple, les problèmes de chemin le plus court présentent une sous-structure optimale. Le chemin le plus court de A à C est le chemin le plus court de A à un nœud B suivi du chemin le plus court de ce nœud B à C.

Plus en détail, pour résoudre un problème de chemin le plus court, vous devrez:

  • trouver les distances du nœud de départ à chaque nœud le touchant (disons de A à B et C)
  • trouver les distances de ces nœuds aux nœuds qui les touchent (de B à D et E, et de C à E et F)
  • nous connaissons maintenant le chemin le plus court de A à E: c'est la somme la plus courte de Ax et xE pour un nœud x que nous avons visité (B ou C)
  • répéter ce processus jusqu'à ce que nous atteignions le nœud de destination finale

Parce que nous travaillons de bas en haut, nous avons déjà des solutions aux sous-problèmes quand vient le temps de les utiliser, en les mémorisant.

N'oubliez pas que les problèmes de programmation dynamique doivent avoir à la fois des sous-problèmes qui se chevauchent et une sous-structure optimale. La génération de la séquence de Fibonacci n'est pas un problème de programmation dynamique; il utilise la mémorisation parce qu'il a des sous-problèmes qui se chevauchent, mais il n'a pas de sous-structure optimale (car il n'y a pas de problème d'optimisation impliqué).

Nick Lewis
la source
1
À mon humble avis, c'est la seule réponse qui ait du sens en termes de programmation dynamique. Je suis curieux depuis quand les gens ont commencé à expliquer DP en utilisant des nombres de Fibonacci (peu pertinents).
Terry Li
@TerryLi, Ça a peut-être du sens, mais ce n'est pas facile à comprendre. Le problème du nombre de Fibonacci est connu et facile à comprendre.
Ajay
6

Programmation dynamique

Définition

La programmation dynamique (DP) est une technique générale de conception d'algorithmes pour résoudre des problèmes avec des sous-problèmes qui se chevauchent. Cette technique a été inventée par le mathématicien américain "Richard Bellman" dans les années 1950.

Idée clé

L'idée clé est de sauvegarder les réponses des sous-problèmes plus petits qui se chevauchent pour éviter le recalcul.

Propriétés de programmation dynamique

  • Une instance est résolue à l'aide des solutions pour les instances plus petites.
  • Les solutions pour une instance plus petite peuvent être nécessaires plusieurs fois, alors stockez leurs résultats dans une table.
  • Ainsi, chaque instance plus petite n'est résolue qu'une seule fois.
  • Un espace supplémentaire est utilisé pour gagner du temps.
Sabir Al Fateh
la source
5

Je suis également très nouveau dans la programmation dynamique (un algorithme puissant pour un type de problème particulier)

En termes plus simples, il suffit de penser la programmation dynamique comme une approche récursive en utilisant les connaissances précédentes

Connaissances antérieures sont ce qui importe le plus ici, gardez une trace de la solution des sous-problèmes que vous avez déjà.

Considérez ceci, l'exemple le plus basique pour dp de Wikipedia

Trouver la séquence de fibonacci

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

Permet de décomposer l'appel de fonction avec disons n = 5

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

En particulier, fib (2) a été calculé trois fois à partir de zéro. Dans de plus grands exemples, beaucoup plus de valeurs de fib, ou sous-problèmes, sont recalculées, conduisant à un algorithme de temps exponentiel.

Maintenant, essayons en stockant la valeur que nous avons déjà trouvée dans une structure de données, par exemple une carte

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

Ici, nous sauvegardons la solution des sous-problèmes dans la carte, si nous ne l'avons pas déjà. Cette technique de sauvegarde des valeurs que nous avions déjà calculées est appelée mémorisation.

Enfin, pour un problème, essayez d'abord de trouver les états (sous-problèmes possibles et essayez de penser à la meilleure approche de récursivité afin que vous puissiez utiliser la solution du sous-problème précédent dans d'autres).

Aman Singh
la source
Arnaque directe de Wikipedia. Downvoted !!
solidak
4

La programmation dynamique est une technique pour résoudre des problèmes avec des sous-problèmes qui se chevauchent. Un algorithme de programmation dynamique résout chaque sous-problème une seule fois, puis enregistre sa réponse dans une table (tableau). Éviter le travail de recalcul de la réponse à chaque fois que le sous-problème est rencontré. L'idée sous-jacente de la programmation dynamique est la suivante: éviter de calculer deux fois la même chose, généralement en conservant un tableau des résultats connus des sous-problèmes.

Les sept étapes du développement d'un algorithme de programmation dynamique sont les suivantes:

  1. Établissez une propriété récursive qui donne la solution à une instance du problème.
  2. Développer un algorithme récursif selon la propriété récursive
  3. Voir si la même instance du problème est à nouveau résolue et à nouveau dans les appels récursifs
  4. Développer un algorithme récursif mémorisé
  5. Voir le modèle de stockage des données dans la mémoire
  6. Convertir l'algorithme récursif mémorisé en algorithme itératif
  7. Optimiser l'algorithme itératif en utilisant le stockage selon les besoins (optimisation du stockage)
Adnan Qureshi
la source
Est-ce 6. Convert the memoized recursive algorithm into iterative algorithmune étape obligatoire? Cela signifierait que sa forme finale est non récursive?
truthadjustr
ce n'est pas obligatoire, c'est facultatif
Adnan Qureshi
L'objectif est de remplacer l'algorithme récursif utilisé pour stocker les données en mémoire par une itération sur les valeurs stockées car une solution itérative enregistre la création d'une pile de fonctions pour chaque appel récursif effectué.
David C. Rankin
2

en bref la différence entre la mémorisation récursive et la programmation dynamique

La programmation dynamique comme son nom l'indique utilise la valeur calculée précédente pour construire dynamiquement la prochaine nouvelle solution

Où appliquer la programmation dynamique: si votre solution est basée sur une sous-structure optimale et un sous-problème qui se chevauchent, alors dans ce cas, l'utilisation de la valeur calculée plus tôt sera utile pour que vous n'ayez pas à la recalculer. C'est une approche ascendante. Supposons que vous devez calculer fib (n) dans ce cas, tout ce que vous devez faire est d'ajouter la valeur calculée précédente de fib (n-1) et fib (n-2)

Récursion: Fondamentalement, vous subdivisez votre problème en une partie plus petite pour le résoudre facilement, mais gardez-le à l'esprit, cela n'évite pas le recalcul si nous avons la même valeur calculée précédemment dans un autre appel de récursivité.

Mémorisation: Le stockage de l'ancienne valeur de récursion calculée dans le tableau est connu sous le nom de mémorisation, ce qui évitera le recalcul s'il a déjà été calculé par un appel précédent, de sorte que toute valeur sera calculée une fois. Donc, avant de calculer, nous vérifions si cette valeur a déjà été calculée ou non si elle est déjà calculée, puis nous retournons la même chose de la table au lieu de recalculer. C'est aussi une approche descendante

Effort
la source
-1

Voici un exemple simple de code python Recursive, Top-down, Bottom-upapproche pour la série de Fibonacci:

Récursif: O (2 n )

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

De haut en bas: O (n) Efficace pour une entrée plus importante

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

De bas en haut: O (n) pour la simplicité et les petites tailles d'entrée

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))
0xAliHn
la source
Le premier cas N'A PAS une durée d'exécution de n ^ 2, sa complexité temporelle est O (2 ^ n): stackoverflow.com/questions/360748/…
Sam
mis à jour merci. @Sam
0xAliHn