T3.2 Programmation dynamique⚓︎
On a déjà rencontré plusieurs paradigmes algorithmiques:
- l'algorithme glouton : il construit de façon itérative une solution en optimisant un critère de manière locale, sans assurance de construire une solution globale optimale;
- la méthode «diviser pour régner» : on divise un problème en sous-problèmes indépendants qu'on résout récursivement, puis on combine les solutions des sous-problèmes pour construire une solution globale.
Mais que se passe-t-il lorsque les sous-problèmes se chevauchent, c'est-à-dire qu'ils ne sont pas indépendants, comme dans le calcul (naïf) des termes de la suite de Fibonacci (exercice 6 du T2.2)?
Principe
La programmation dynamique consiste à diviser un problème en sous-problèmes indépendants puis à résoudre chaque sous-problème du plus petit au plus grand en stockant au fur et à mesure les résultats afin de ne pas avoir à les recalculer.
Cette technique a été développée par Richard Bellman dans les années 1950 chez RAND Corporation et permet de résoudre de nombreux problèmes d'optimisation de façon très performante.
Nous illustrerons cette technique par deux procédés: la mémoïsation et la méthode «bottom-up».
1. Principe de mémoïsation⚓︎
La mémoïsation consiste tout simplement à stocker les valeurs déjà calculées par une fonction pour des «petits» arguments. On peut pour cela utiliser un tableau ou un dictionnaire.
Exercice 1
Écrire une fonction fibo
récursive et mémoïsée: on commence par consulter le dictionnaire pour savoir si la valeur est connue, auquel cas on se contente de la renvoyer, sinon on la calcule comme naïvement.
Rappel de la fonction naïve:
1 2 3 4 5 |
|
Python's corner
En Python il existe un décorateur qui permet de mémoïser automatiquement une fonction:
1 2 3 4 5 6 7 8 |
|
Exercice 2
La fonction suivante donne le nombre minimal de pièces pour payer la somme demandée (cf. le problème du rendu de monnaie):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
-
Tester la fonction avec les appels:
rendu_monnaie(12, [1, 2, 5, 10, 20, 50, 100])
rendu_monnaie(28, [1, 2, 5, 10, 20, 50, 100])
rendu_monnaie(37, [1, 2, 5, 10, 20, 50, 100])
-
Mémoïser la fonction
rendu_monnaie
:- initialiser correctement un dictionnaire
memo_rendu
; - consulter d'abord le dictionnaire pour voir si
somme
lui appartient; - sinon calculer la valeur
rendu
de la même façon et l'ajouter au dictionnairememo_rendu
avant de la renvoyer.
- initialiser correctement un dictionnaire
2. Méthode «bas-haut» («bottom-up»)⚓︎
Dans la méthode «bottom-up» on va construire un tableau (1D ou 2D) comme dans la méthode de mémoïsation, mais en commençant par les «petits» résultats pour aller vers la solution globale. On peut alors (parfois) transformer un programme récursif en un programme itératif.
Exemple avec Fibonacci
1 2 3 4 5 |
|
Exercice 3
Adapter la méthode «bottom-up» pour la fonction rendu_monnaie
précédente en complétant le code suivant:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Exercice 4
Sur une grille rectangulaire, combien de chemins mènent du coin supérieur gauche au coin inférieur droit, en se déplaçant uniquement le long des traits horizontaux vers la droite et le long des traits verticaux vers le bas?
- Écrire une fonction récursive
nb_chemins_rec
qui répond au problème puis la «mémoïser». - Écrire une fonction itérative
nb_chemins
selon la méthode «bottom-up» qui utilise un tableau 2D.
On remarquera:
- qu'un déplacement uniquement vertical ou uniquement horizontal ne donne qu'un seul chemin.
- que pour arriver en bas à droite, on ne peut arriver que de deux endroits: juste au-dessus ou juste à gauche.
3. Un problème pour finir en beauté⚓︎
Dans l'image ci-dessous, quel est la taille du plus grand carré blanc, c'est-à-dire qui ne contient aucun pixel noir?