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.
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.
Réponses:
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.
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
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)
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]
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?
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
if n in cache
comme avec l'exemple de haut en bas ou est-ce que je manque quelque chose.
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).
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.
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:
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é).
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
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).
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:
6. Convert the memoized recursive algorithm into iterative algorithm
une étape obligatoire? Cela signifierait que sa forme finale est non récursive?
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
Voici un exemple simple de code python Recursive
, Top-down
, Bottom-up
approche pour la série de Fibonacci:
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))
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))
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))