Niveau : Terminale générale, enseignement de spécialité NSI
Définition
La programmation dynamique est un principe algorithmique permettant d'améliorer la complexité temporelle de la résolution d'un problème dans certaines situations.
Cette définition est un peu vague et nous en verrons une plus précise par la suite. Nous retrouverons dans cette partie des notions de récursivité et même des similitudes avec le principe de diviser pour régner.
Nous allons donc tenter de ...
Mais nous allons commencer par un exemple que vous avez déjà vu en récursivité.
La suite de Fibonacci a été inventée au Moyen-Age dans le but de tenter de modéliser l'évolution d'une population de lapins ( plus de détails ici par exemple )
Elle est définie ainsi :
→ en dehors des deux premiers cas, chaque terme de la suite de Fibonacci est donc égal à la somme des deux termes précédents :
n | Fn |
---|---|
0 | 0 |
1 | 1 |
2 | 0 + 1 = 1 |
3 | 1 + 1 = 2 |
4 | 1 + 2 = 3 |
5 | 2 + 3 = 5 |
6 | 3 + 5 = 8 |
... | ... |
Vous avez déjà programmé cette suite grâce à une fonction récursive que vous pouvez voir ci-dessous :
Cette écriture est très simple, et très lisible : on retrouve bien à la ligne 13 la définition de la suite de Fibonacci pour n >=2
Cependant, la complexité en temps de cette fonction est très mauvaise, elle est de l'ordre de 2n (exactement 1,6n), c'est à dire exponentielle.
Ne cherchez même pas à calculer le terme de rang n = 1000, le temps est déjà très long pour n = 50 !
fib(6)
.
Une fois que tout ces appels sont réalisés, il faut bien sur les additionner en "remontant l'arbre"...Vous pouvez imaginer la complexité du processus pour fib(30)
!
On peut remarquer qu'ici, si la récursivité présente un code particulièrement lisible, mais ce code n'est pas efficace du tout !
Si l'on regarde de plus près l'arbre précédent, on peut constater que l'on fait plusieurs fois la même chose :
fib(4)
et même si l'on analyse encore plus ...
On peut remarquer que l'on calcule 3 fois fib(3)
et 5 fois fib(2)
...
Nous allons travailler par mémoïsation (non ce n'est pas une faute de frappe !), c'est à dire que l'on va conserver les résultats intermédiaires dans un tableau afin de remplacer un nouveau calcul par un appel mémoire, beaucoup plus économe en temps.
La mémoïsation fait partie de la programmation dynamique.
Le code précédent propose une approche mettant en œuvre la récursivité et la mémoïsation, c'est une des deux approches de la programmation dynamique. Elle est appelée aussi "du haut vers le bas" ou encore "top-down" car elle part du haut de l'arbre des appels récursifs vu précédemment.
Les deux codes de calcul de la suite de Fibonacci ne sont pas si différents finalement. Nous allons tenter de tester leur efficacité.
Il existe aussi une autre approche possible en programmation dynamique, "du bas vers le haut" ou encore "bottom-up". Cette approche consiste à parcourir l'arbre ds appels récursifs en partant du bas.
On remarque alors que dans ce cas, il n'est plus nécessaire de faire appel à la récursivité.
Le code correspondant pour calculer la suite de Fibonacci serait :
On peut également remarquer que :
Contrairement à la récursivité, il n'est pas aisé de reconnaître la programmation dynamique dans un programme ou un algorithme.
Cependant à chaque fois que l'on utilise la récursivité il est important de se demander si une optimisation mettant en œuvre la programmation dynamique n'est pas envisageable.
Pour concevoir une fonction en programmation dynamique, il faut le plus souvent:
Nous allons maintenant travailler sur un problème d'optimisation déjà rencontré l'année dernière : le problème du rendu de monnaie.
Petit rappel : vous avez à votre disposition un nombre illimité de pièces de 2 cts, 5 cts, 10 cts, 50 cts et 1 euro (100 cts). Vous devez rendre une certaine somme (rendu de monnaie). Le problème est le suivant : "Quel est le nombre minimum de pièces qui doivent être utilisées pour rendre la monnaie"
La résolution "gloutonne" de ce problème peut être la suivante :
Prenons un exemple :
nous avons 1 euro 77 cts à rendre :
L'algorithme se termine en renvoyant 6 (on a dû rendre 6 pièces)
Voici la fonction récursive du rendu de momnaie glouton :
rendu_monnaie_rec(pieces,177)
demande beaucoup trop d'appel récursif et plante le programme.
Comme vous avez peut-être déjà dû le remarquer, même dans le cas simple évoqué ci-dessus (11 cts à rendre), nous faisons plusieurs fois exactement le même calcul. Par exemple on retrouve 2 fois la branche qui part de "4" :
Il va donc être possible d'appliquer la même méthode que pour Fibonacci : la programmation dynamique.
À noter que dans des cas plus "difficiles à traiter" comme 1,71 euro, on va retrouver de nombreuses fois exactement les mêmes calculs, il est donc potentiellement intéressant d'utiliser la programmation dynamique.
Compléter la version dynamique de la fonction rendu_monnaie_rec
ci-dessous :
Nous allons considérer qu'une scierie reçoit des planches de longueur l entière.
La scierie découpe ces planches en morceaux de longueur également entière et les revend selon selon une grille tarifaire donnée :
longueur (m) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
prix (€) | 0 | 1 | 5 | 8 | 9 | 10 | 15 | 17 | 20 | 24 | 30 |
L'objectif est donc de découper chaque planche de façon optimale pour en tirer un prix maximum.
Afin de bien appréhender le problème répondez aux questions ci dessous :
Nous allons tenter de coder une fonction coupe_recurs
ayant pour argument la longueur n
de la planche et le tableau p
des prix et qui renverra la revenu optimal que l'on peut tirer de la planche.
p[i]
et de coupe_recurs(n-i,p)
?Comme d'habitude, la solution récursive est simple à coder mais n'est pas très efficace....
Nous pouvons constater que nous sommes en présence d'un problème d'optimisation.
Nous allons maintenant chercher les sous problèmes et leurs chevauchements.
Fond : Texte : Tables : Thème du langage:
Contenu
sous
licence CC BY-NC-SA 3.0
villemin gerard et pixees
Mise à jour le 14/05/2023