web-dev-qa-db-fra.com

Qu'est-ce que la programmation dynamique?

Qu'est-ce que la programmation dynamique ?

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

J'ai lu le article de wikipedia dessus, mais je ne le comprends toujours pas.

259
unknown

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

Un bon exemple consiste à résoudre 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 très utilisée dans les problèmes de chaîne, tels que le problème d'édition de chaîne. Vous résolvez un ou plusieurs sous-ensembles du problème, puis utilisez ces informations pour résoudre le problème d'origine le plus difficile.

Avec la programmation dynamique, vous stockez vos résultats dans une sorte de tableau. Lorsque vous avez besoin de la réponse à un problème, vous référencez la table et voyez si vous savez déjà ce que c'est. Sinon, vous utilisez les données de votre tableau pour vous donner un tremplin vers la réponse.

Le livre Cormen Algorithms contient un chapitre important sur la programmation dynamique. ET c'est gratuit sur Google Books! Vérifiez-le ici.

195
samoz

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 th numéro de fibonacci défini par

Fn = Fn-1 + Fn-2 et F = 0, F1 = 1

Récursion

La manière évidente de faire cela 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 - Memoization

La récursivité fait beaucoup de calculs inutiles, car un nombre donné de fibonacci sera calculé plusieurs fois. Un moyen facile d'améliorer cela consiste à 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 supprimer la récursion 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:

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écursion dans le problème.
    2. Top-down: stocke la réponse pour chaque sous-problème dans un tableau pour éviter d'avoir à les recalculer.
    3. Bottom-up: 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 gauche à droite, tels que des chaînes, des arbres ou des séquences entières. Si l'algorithme naïf récursif ne calcule pas le même sous-problème plusieurs fois, la programmation dynamique ne vous aidera pas.

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

147
Tristan

Voici ma réponse r dans un sujet similaire

Commencer avec

Si vous voulez tester vous-même mes choix concernant les juges en ligne sont

et bien sur

Vous pouvez également vérifier les bons cours d'algorithmes des universités

Après tout, si vous ne pouvez pas résoudre les problèmes, demandez à SO qu'il existe ici de nombreux algorithmes addict

62
user467871

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, avec les mêmes entrées). La complexité algorithmique ne fait aucune différence avant que les résultats ne soient stockés.

La récursivité est la méthode d'une fonction qui s'appelle elle-même, généralement avec un jeu de données plus petit. Étant donné que la plupart des fonctions récursives peuvent être converties en fonctions itératives similaires, la complexité algorithmique ne change rien non plus.

La programmation dynamique consiste à résoudre des problèmes secondaires plus faciles à résoudre et à construire la réponse à partir de cela. La plupart des algorithmes DP seront dans les temps d'exécution entre un algorithme de Greedy (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 la récursivité, mais ils ne doivent pas l'être.
  • Les algorithmes DP ne peuvent pas être accélérés par la mémorisation, car chaque sous-problème n'est résolu qu'une seule fois (ou la fonction "résoudre" appelée).
36
philomathohollic

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

Alors qu'un algorithme Greedy est généralement appelé naive , car il peut s'exécuter plusieurs fois sur le même ensemble de données, la programmation dynamique évite cet écueil en approfondissant la compréhension des résultats partiels qui doivent être fournis. stockés pour aider à construire la solution finale.

Un exemple simple consiste à parcourir une arborescence ou un graphique uniquement à travers les nœuds qui contribueraient à la solution, ou à insérer dans un tableau les solutions que vous avez trouvées jusqu'à ce que vous puissiez éviter de parcourir les mêmes nœuds à plusieurs reprises.

Voici un exemple de problème adapté à la programmation dynamique, selon le 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 de l'ouvrage Programming Challenges (Défis de programmation), je vous suggère de le vérifier.

Regardez bien ce problème, si nous définissons une fonction de coût nous indiquant la distance qui sépare deux chaînes, nous devons considérer les trois types naturels de modifications:

Substitution - remplacez un caractère du motif "s" par un caractère différent dans le texte "t", par exemple en changeant "plan" en "point".

Insertion - insérez un seul caractère dans le motif "s" pour l'aider à correspondre au texte "t", par exemple en remplaçant "ago" par "agog".

Suppression - Supprimez un seul caractère du modèle "s" pour l'aider à correspondre au texte "t", par exemple si vous modifiez "heure" en "notre".

Lorsque nous définissons chacune des opérations comme coûtant une étape, nous définissons la distance d'édition entre deux chaînes. Alors, comment le calculons-nous?

Nous pouvons définir un algorithme récursif en observant que le dernier caractère de la chaîne doit être soit apparié, substitué, inséré ou supprimé. Couper les caractères lors de la dernière opération d'édition laisse une opération de paire 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éterminer quelle option conduit à la meilleure solution et choisir cette option en conséquence. Nous pouvons apprendre ce coût, grâce à l'impressionnant 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 est également incroyablement lent.

Sous 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.

Pourquoi l'algorithme est-il si lent? Cela prend un temps exponentiel, car il recalcule les valeurs encore et encore. À chaque position de la chaîne, la récursivité se divise en trois directions, ce qui signifie qu’elle croît à un rythme d’au moins 3 ^ n, voire plus vite, car la plupart des appels réduisent 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 sont des calculs qui ont déjà été calculés auparavant. Comment savons-nous? Eh bien, il ne peut y avoir que | s | · | T | possibles appels récursifs uniques, puisqu’il n’ya que le nombre de paires distinctes (i, j) pouvant servir de paramètres aux appels récursifs.

En stockant les valeurs de chacune de ces paires (i, j) dans un tableau, nous pouvons éviter de les recalculer et les rechercher au besoin.

Le tableau est une matrice bidimensionnelle m où chacun des | s | · | t | cellules 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.

D'abord, il obtient ses valeurs intermédiaires en utilisant une table de recherche 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 de montage ultérieurement.

** Troisièmement, ** Troisièmement, il utilise une fonction de cible plus générale () au lieu de simplement renvoyer m [| s |] [| t |] .cost. Cela nous permettra d’appliquer cette routine à un plus grand nombre de problèmes.

Ici, une analyse très particulière de ce qui est nécessaire pour rassembler les résultats partiels les plus optimaux est ce qui fait de la solution une solution "dynamique".

Voici une solution alternative complète au même problème. C'est aussi un "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é traité avec tant d’efficacité.

20
andandandand

Les éléments 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 plus court chemin de A à C est le plus court chemin de A à un nœud B suivi du plus court chemin 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 (par exemple de A à B et C)
  • trouver les distances entre ces nœuds et les 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 A-x et x-E pour un nœud x visité (B ou C)
  • répéter ce processus jusqu'à atteindre le nœud de destination final

Parce que nous travaillons de bas en haut, nous avons déjà des solutions aux problèmes secondaires au moment 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 comporte des sous-problèmes qui se chevauchent, mais sa sous-structure n'est pas optimale (car il n'y a pas de problème d'optimisation).

11
Nick Lewis

Voici un tutoriel de Michael A. Trick de la CMU que j'ai trouvé particulièrement utile:

http://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, en particulier CLR et Kleinberg, les Tardos sont très bonnes!).

La raison pour laquelle j'aime ce tutoriel, c'est parce qu'il introduit les concepts avancés assez progressivement. C'est un matériau un peu vieux, mais c'est un bon ajout à la liste de ressources présentée ici.

Consultez également la page de Steven Skiena et des conférences sur la programmation dynamique: http://www.cs.sunysb.edu/~algorith/video-lectures/

http://www.cs.sunysb.edu/~algorith/video-lectures/1997/lecture12.pdf

5
Edmon

Programmation dynamique

Définition

La programmation dynamique (DP) est une technique générale de conception d’algorithmes permettant de résoudre des problèmes comportant 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 principale est de sauvegarder les réponses des sous-problèmes plus petits qui se chevauchent pour éviter un nouveau calcul.

Propriétés de programmation dynamique

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

Je suis également très novice en programmation dynamique (un algorithme puissant pour certains types de problèmes)

Dans la plupart des mots simples, il suffit de penser à la programmation dynamique comme une approche récursive avec l’utilisation de connaissances précédentes

La connaissance précédente est ce qui compte le plus ici. Gardez une trace de la solution des sous-problèmes que vous avez déjà.

Considérez ceci, exemple le plus fondamental pour dp de Wikipedia

Trouver la séquence 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 dites 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 des exemples plus grands, beaucoup plus de valeurs de fib, ou de sous-problèmes, sont recalculées, ce qui conduit à un algorithme temporel exponentiel.

Maintenant, essayons en stockant la valeur que nous avons déjà trouvée dans une structure de données, disons un Map

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]

Nous enregistrons ici la solution des sous-problèmes sur 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 d’utiliser la solution du sous-problème précédent dans d’autres).

4
Aman Singh

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

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émo
  5. Voir le modèle en stockant les données dans la mémoire
  6. Convertir l'algorithme récursif mémoisé en algorithme itératif
  7. Optimiser l'algorithme itératif en utilisant le stockage selon les besoins (optimisation du stockage)
3
Adnan Qureshi

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

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

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, il sera utile d'utiliser la valeur calculée précédemment pour vous éviter de la recalculer. C'est une approche ascendante. Supposons que vous ayez besoin de calculer fib (n). Dans ce cas, il vous suffit d’ajouter la valeur calculée précédente de fib (n-1) et de fib (n-2).

Récursion: Subdiviser votre problème en une partie plus petite pour le résoudre facilement, mais gardez-le à l'esprit ne permet pas d'éviter un nouveau calcul si nous avons la même valeur calculée précédemment dans un autre appel de récursivité.

Mémoisation: Stocker la valeur de récursion calculée dans une table s'appelle une mémoisation, ce qui évite le nouveau calcul si elle avait déjà été calculée par un appel précédent. Toute valeur sera donc calculée une fois. Donc, avant de calculer, nous vérifions si cette valeur a déjà été calculée ou pas, si elle est déjà calculée, nous renvoyons la même chose depuis table au lieu de recalculer. C'est aussi une approche descendante

1
Endeavour