TD1 - Le rendu de monnaie
- Comprendre les limites d’un algorithme glouton ;
- Explorer toutes les solutions d’un problème ;
- Identifier les calculs redondants ;
- Introduire la programmation dynamique (mémoïsation).
Le TD se fait sur papier.
On cherche à rendre une somme d'argent avec un nombre minimal de pièces, à partir d’un système de pièces donné.
C’est un problème d’optimisation.
Approche gloutonne : une bonne idée ?
On considère les pièces suivantes :
1, 2, 5, 10, 20, 50, 100, 200
Un algorithme glouton consiste à prendre à chaque étape la plus grande pièce possible.
Exemple : rendre 13€.
- 10€ → reste 3€
- 2€ → reste 1€
- 1€ → reste 0€
➡️ Total : 3 pièces
- 1Appliquer l’algorithme glouton pour rendre 28€.
- 2Même question sans pièce de 1€, pour 36€.
- 3Dans quels cas l’algorithme glouton semble-t-il fonctionner ?
Un algorithme glouton :
- ne revient jamais en arrière ;
- peut donner une solution non optimale, voire aucune solution.
👉 Il faut donc envisager une autre approche.
Explorer toutes les solutions
On cherche maintenant toutes les façons possibles de rendre une somme.
Exemple : rendre 5€
- 5 → 1 pièce
- 2 + 2 + 1 → 3 pièces
- 2 + 1 + 1 + 1 → 4 pièces
- 1 + 1 + 1 + 1 + 1 → 5 pièces
👉 On peut représenter cela avec un arbre de possibilités.
- 1Combien existe-t-il de façons différentes de rendre 5€ ?
- 2Quelle est la solution utilisant le moins de pièces ?
- 3Quelle est la solution utilisant le plus de pièces ?
On considère maintenant les pièces : 2, 5, 10
- 4Lister toutes les façons de rendre 12€.
- 5Quelle est la solution optimale ?
- 6Dessiner l’arbre des possibilités.
Explorer toutes les solutions :
- garantit de trouver la meilleure,
- mais devient très coûteux quand le montant augmente.
👉 Pourquoi ?
Construire une solution progressive
👉 On construit maintenant les solutions de bas en haut (Approche bottom-up).
On considère les pièces : 1, 2, 5 ainsi que le tableau suivant, où la première ligne correspond au montant à rendre et la deuxième ligne au nombre minimal de pièces nécessaires pour ce montant :
| Montant (€) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Nb pièces min | 0 | 1 | 1 |
- 1Compléter le tableau jusqu’à 6€.
- 2Pour calculer 6€, quelles valeurs déjà connues utilise-t-on ?
- 3En déduire une règle générale pour calculer le nombre minimal de pièces pour un montant donné.
Pour calculer une valeur :
- on utilise des résultats déjà connus ;
- on évite les recalculs.
👉 C’est le principe de la programmation dynamique.
Vers la programmation dynamique
On considère cet algorithme récursif :
liste_Piece = [1,2,5,10,20,50,100,200]
def rendu_monnaie_rec(montant : int) -> int:
'''
Retourne le nombre minimal de pièces nécessaires pour rendre le montant donné.
:param montant: le montant à rendre
:return: le nombre minimal de pièces nécessaires pour rendre le montant donné
'''
if montant == 0:
return 0
else:
mini = 1000
for i in range(len(liste_Piece)):
if liste_Piece[i] <= montant:
nbPiece = 1 + rendu_monnaie_rec(montant - liste_Piece[i])
if nbPiece < mini:
mini = nbPiece
return mini
- 1Expliquer le fonctionnement de cet algorithme.
- 2Pourquoi cet algorithme refait-il plusieurs fois les mêmes calculs ?
- 3Faire le lien avec les répétitions observées dans l’arbre.
Ajouter de la mémoire (mémoïsation)
- 1Quelle information faudrait-il mémoriser pour éviter les recalculs ?
- 2À quel moment du programme faudrait-il consulter cette mémoire ?
- 3À quel moment faudrait-il la mettre à jour ?
Compléter l’algorithme
def rendu_monnaie(pieces : list, montant : int) -> int:
'''
Retourne le nombre minimal de pièces nécessaires pour rendre le montant donné, en utilisant la mémoïsation.
:param pieces: la liste des pièces disponibles
:param montant : le montant à rendre
:return: le nombre minimal de pièces nécessaires pour rendre le montant donné
'''
memoire = [...] # Doit initialiser une liste avec des valeurs -1 pour indiquer que les résultats ne sont pas encore calculés
return rendu_monnaie_dynamique(..., ..., ...)
def rendu_monnaie_dynamique(pieces : list, montant : int, memoire : list) -> int:
'''
Retourne le nombre minimal de pièces nécessaires pour rendre le montant donné, de façon dynamique.
:param pieces: la liste des pièces disponibles
:param montant: le montant à rendre
:param memoire: une liste pour mémoriser les résultats déjà calculés
:return: le nombre minimal de pièces nécessaires pour rendre le montant donné
'''
if ... : #Quand on a plus rien à rendre
return 0
elif ... : #Si on a déjà calculé le résultat pour ce montant, on le retourne
return ...
else:
mini = 1000
for i in range(len(...)):
if pieces[i] <= ... :
nb = 1 + rendu_monnaie_dynamique(..., ..., ...)
if nb < mini:
mini = nb
memoire[...] = ... #Mise à jour de la mémoire
return ...
- 1Compléter les parties manquantes de l’algorithme.
- 2Expliquer le rôle de
memoire. - 3Que permet d'éviter la condition sur la mémoire ?
- 4En quoi cet algorithme est-il plus efficace que la version récursive simple ?
Comparaison des performances
- 1Quelle version est la plus rapide ? Justifier.
- 2Résumer en une phrase la différence entre les deux approches.
- 3Donner une définition simple de la programmation dynamique à partir de ce TD.
Tester les deux algorithmes pour rendre 1000€ avec les pièces suivantes : 1, 2, 5, 10, 20, 50, 100, 200 sur python, et, à l'aide de la bibliothèque time, mesurer le temps d'exécution de chaque algorithme.