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é naïf Comme elle peut être exécutée plusieurs fois sur le même ensemble de données, la programmation dynamique évite cet écueil grâce à une meilleure compréhension des résultats partiels qui doivent être stockés pour aider à construire la solution finale.
Un exemple simple est la traversée d'un arbre ou d'un graphe uniquement à travers les nœuds qui contribueraient à la solution, ou la mise dans un tableau des solutions que vous avez trouvées jusqu'à présent afin d'éviter de traverser les mêmes nœuds encore et encore.
Voici un exemple de problème adapté à la programmation dynamique, tiré du juge en ligne de l'UVA : Editer l'échelle des marches.
Je vais faire un bref résumé de la partie importante de l'analyse de ce problème, tirée du livre Programming Challenges, je vous suggère de le consulter.
Regardez bien ce problème, si on définit une fonction de coût qui nous dit la distance entre deux cordes, nous devons considérer les trois types naturels types de changements :
Substitution - changer un seul caractère unique du motif "s" à un caractère différent dans le texte "t", par exemple par exemple, remplacer "shot" par "spot".
Insertion - insérer un seul caractère dans le motif "s" pour l'aider à correspondre au texte "t", par exemple en remplaçant "ago" par "agog".
Suppression - supprime un seul caractère du motif "s" pour l'aider à correspondre au texte "t", par exemple en remplaçant "hour" par "our".
Lorsque nous définissons chacune de ces opérations pour coûtent une étape, nous définissons la distance d'édition entre deux chaînes de caractères. Alors comment la calculons-nous ?
Nous pouvons définir un algorithme récursif en utilisant l'observation que le dernier caractère de la chaîne doit être soit correspondre, substitué, inséré ou supprimé. La suppression des caractères dans la dernière opération d'édition laisse une laisse une paire de chaînes de caractères plus petites. Soit i et j le dernier caractère du préfixe pertinent de et t, respectivement. il existe trois paires de chaînes plus courtes après la dernière opération, correspondant à la chaîne après une correspondance/substitution, insertion ou suppression. Si nous connaissions le coût de l'édition des trois paires de petites chaînes, 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 à cette impressionnante chose qu'est la récursion :
#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 aussi incroyablement lent.
Sur notre ordinateur, il faut plusieurs secondes pour comparer deux chaînes de 11 caractères, et le calcul disparaît dans jamais sur une plus longue période.
Pourquoi l'algorithme est-il si lent ? Il prend temps exponentiel car il recalcule les valeurs encore et encore et encore. Sur chaque position dans la chaîne, la récursion se ramifie de trois façons, ce qui signifie qu'elle croît à un rythme d'au moins 3^n - en fait, encore plus vite puisque la plupart des ne réduisent qu'un seul des deux indices, et non 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à déjà été calculées auparavant. Comment pouvons-nous savons-nous ? Eh bien, il ne peut y avoir que |s| - |t| appels récursifs uniques possibles, puisqu'il n'y a que ce nombre de paires (i, j) distinctes pouvant servir de paramètres des appels récursifs.
En stockant les valeurs de chacune de ces paires (i, j) dans un tableau, nous pouvons éviter de les recalculer et simplement les les consulter en cas de besoin.
Le tableau est une matrice bidimensionnelle m où chacune des cellules |s|-|t| contient le coût de la solution optimale. contient le coût de la solution optimale solution optimale de ce sous-problème, ainsi que ainsi qu'un pointeur parent expliquant comment nous sommes arrivés à cet endroit :
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 récursive.
D'abord, il obtient ses valeurs intermédiaires en utilisant la consultation de tables au lieu de 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é en utilisant une fonction plus générale de cellule de but() plus générale, au lieu de retourner simplement m[|s|][|t|].cost. Cela nous permettra d'appliquer cette routine à une classe plus de problèmes.
Ici, c'est une analyse très particulière de ce qu'il faut faire pour obtenir les résultats partiels les plus optimaux qui fait de la solution une solution "dynamique".
Voici une solution alternative et complète au même problème. Il s'agit également d'une solution "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 de l'UVA. Je trouve étonnant qu'un problème aussi lourd ait été résolu de manière aussi efficace.