Un commerçant cherche à rendre la monnaie à ses clients de façon optimale, c’est-à-dire avec le nombre minimal de pièces et de billets.
Dans ce problème,
On suppose que les clients ne donnent que des sommes entières en euros (pas de centimes pour simplifier) ;
Les valeurs des pièces et billets à disposition sont : 1, 2, 5, 10, 20, 50, 100, 200 et 500. On suppose que l’on a autant d’exemplaires que nécessaire de chaque pièce et billet ;
Dans la suite, afin de simplifier, on désigne par « pièces » à la fois les pièces et les billets.
Algorithme glouton
Un client nous achète un objet qui coûte 53 euros. Il paye avec un billet de 200 euros. Il faut donc lui rendre 147 euros, par exemple un billet de 100, deux billets de 20, un billet de 5 et une pièce de 2.
Afin de minimiser le nombre de pièces à rendre, on peut suivre la stratégie suivante :
On commence par rendre la pièce de plus grande valeur possible ;
On déduit cette valeur de la somme (encore) à rendre ;
On recommence, jusqu’à obtenir une somme nulle.
En procédant ainsi, on résout le problème étape par étape et un choix optimal est effectué à chacune de ces étapes (la pièce de plus grande valeur). Cet algorithme fait parti de la catégorie des algorithmes gloutons : chercher la solution optimale à partir d’optima locaux.
Importer le module typing au début du fichier :
1
fromtypingimportList
Préparer le code suivant et étudier sa structure :
1
2
3
4
5
6
7
8
9
10
11
12
13
if__name__=="__main__":# valeurs des piècespieces=[1,2,5,10,20,50,100]prix=int(input("Quel est le prix ? "))client=int(input("Combien le client donne-t-il ? "))a_rendre=somme_a_rendre(prix,client)comment=pieces_a_rendre(a_rendre,pieces)reponse=["{} piece(s) de {}".format(comment[i],pieces[i])foriinrange(len(pieces))]print(f"Je dois rendre : {a_rendre}")print(f"Je donne donc : {reponse}")
Définir la fonction somme_a_rendre dont la spécification est :
1
2
3
4
5
defsomme_a_rendre(prix:int,montant_client:int)->int:"""
Détermine la somme à rendre à partir du prix et du montant donné par
le client.
"""
Réponse
1
2
3
4
5
6
defsomme_a_rendre(prix:int,montant_client:int)->int:"""
Détermine la somme à rendre à partir du prix et
du montant donné par le client.
"""returnmontant_client-prix
Définir la fonction pieces_a_rendre dont la spécification est :
1
2
3
4
5
6
7
defpieces_a_rendre(somme:int,pieces:List[int])->List[int]:"""
Détermine les pièces (et leur nombre) à choisir pour rendre la somme
passée en argument.
Utilise la division euclidienne et le modulo de façon à pouvoir
traiter tous les cas.
"""
defpieces_a_rendre(somme:int,pieces:List[int])->List[int]:"""
Détermine les pièces (et leur nombre) à choisir
pour rendre la somme passée en argument.
Utilise la division euclidienne et le modulo de
façon à pouvoir traiter tous les cas.
On reçoit : pieces = [1, 2, 5, 10, 20]
On retourne : a_rendre = [x, z, y, a, b]
"""a_rendre=[]foriinrange(len(pieces)-1,-1,-1):nbre=somme//pieces[i]somme=somme%pieces[i]a_rendre.append(nbre)returnlist(reversed(a_rendre))defpieces_a_rendre_2(somme:int,pieces:List[int]):a_rendre={}i=len(pieces)-1whilesomme>0:nbre=somme//pieces[i]somme=somme%pieces[i]a_rendre[pieces[i]]=nbrei=i-1returna_rendredefpieces_a_rendre_3(somme:int,pieces:List[int])->List[int]:a_rendre=[0]*len(pieces)i=len(pieces)-1whilesomme>0:nbre=somme//pieces[i]somme=somme%pieces[i]a_rendre[i]=nbrei=i-1returnlist(reversed(a_rendre))
On note $nb$ le nombre de pièces à rendre, prises dans la liste des pièces $p = [1, 2, 5, 10, 20, 50, 100, 200, 500]$, lorsque la somme à rendre est $x$.
Écrire la relation de récurrence que doit vérifier $nb$.
Réponse
$$
nb(x) =
\begin{cases}
0 & \text{ si } x = 0\cr
1 + nb(x - p[i]) & \text{ si } x - p[i] \geqslant 0 \cr
\end{cases}
$$
En fait, on cherche le nombre minimal de pièces à rendre pour la somme $x$.
Adapter la relation de récurrence.
Réponse
$$
nb(x) =
\begin{cases}
0 & \text{ si } x = 0\cr
1 + \min{(nb(x - p[i]))} & \text{ si } x - p[i] \geqslant 0 \text{ et } i \in [0, \text{len}(p)[ \cr
\end{cases}
$$
Écrire le schéma des appels récursifs pour $x=11$ et $p=[2,5,10]$.
Réponse
Indiquer l’avantage et l’inconvénient de l’algorithme par Brute Force.
Réponse
Cet algorithme retourne une solution optimale mais calcule plusieurs fois les mêmes valeurs. Il est donc inutilisable si la valeur à retourner est importante.
Réfléchir au code de la fonction dont la spécification est :
1
2
3
4
5
6
7
defnb_rendre(a_rendre:int,pieces:List[int])->int:"""
Détermine le nombre minimal de pièces
à rendre.
Algorithme récursif.
"""
Écrire le code de cette fonction.
Réponse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defnb_rendre(a_rendre:int,pieces:List[int])->int:"""
Détermine le nombre minimal de pièces
à rendre.
Algorithme récursif.
"""ifa_rendre==0:return0nb_min=float('inf')foriinrange(len(pieces)):ifpieces[i]<=a_rendre:nb=1+nb_rendre(a_rendre-pieces[i],pieces)ifnb<=nb_min:nb_min=nbreturnnb_min
Tester le code pour une somme à rendre de 13 puis de 130. Le résultat confirme-t-il la réponse à la question 4 ?
On souhaite maintenant écrire le code de la fonction pieces_a_rendre dont la spécification est :
1
2
3
4
5
6
7
8
9
10
defpieces_a_rendre(somme:int,pieces:Tuple[int],serie:List[int],series:List[Tuple[int]])->None:"""
Détermine toutes les combinaisons de pièces qui permettent de rendre la somme somme.
pieces est le tuple des pièces disponibles.
series est la liste des pièces à rendre.
series est la liste des listes des pièces à rendre.
"""
Remarque
On peut bien sur créer une fonction enveloppe qui ne prendrait que la somme et la liste des pièces disponibles en argument et retournerait la liste des listes de pièces.
defpieces_a_rendre(somme:int,pieces:Tuple[int],serie:List[int],series:List[Tuple[int]])->None:"""
Détermine toutes les combinaisons de pièces qui permettent de rendre la somme somme.
"""ifsomme==0:series.append(serie)returnNoneforpieceinpieces:ifpiece>somme:continue# Sauvegarde de la liste des pièces pour le retour arrièreserie_sauv=list(serie)serie.append(piece)pieces_a_rendre(somme-piece,pieces,serie,series)serie=serie_sauv
Tester le code pour une somme à rendre de 13 puis de 130. Obtient-on rapidement un résultat pour cette dernière valeur ?
Comment déterminer le nombre minimum de pièces à retourner ?
Réponse
Le nombre minimum de pièces est égal à la plus petite longueur des listes éléments de la liste series.
Programmation dynamique (mémoïsation)
Rappeler quel est l’objectif de la programmation dynamique.
Réponse
La programmation dynamique, comme la méthode diviser-pour-régner, résout des problèmes en combinant des solutions de sous-problèmes.
Les algorithmes diviser-pour-régner partitionnent le problème en sous-problèmes indépendants qu’ils résolvent récursivement, puis combinent leurs solutions pour résoudre le problème initial.
La programmation dynamique, quant à elle, peut s’appliquer même lorsque les sous-problèmes ne sont pas indépendants, c’est-à-dire lorsque des sous-problèmes ont des sous-sous-problèmes communs.
Dans ce cas, un algorithme diviser-pour-régner fait plus de travail que nécessaire, en résolvant plusieurs fois le sous-sous-problème commun. Un algorithme de programmation dynamique résout chaque sous-sous-problème une seule fois et mémorise sa réponse dans un tableau, évitant ainsi le recalcul de la solution chaque fois que le sous-sous-problème est rencontré.
La programmation dynamique est, en général, appliquée aux problèmes d’optimisation. Dans ce type de problèmes, il peut y avoir de nombreuses solutions possibles. Chaque solution a une valeur, et on souhaite trouver une solution ayant la valeur optimale (minimale ou maximale). Une telle solution est une solution optimale au problème, et non pas la solution optimale, puisqu’il peut y avoir plusieurs solutions qui donnent la valeur optimale.
Le manque d’efficacité de la méthode force brute a pour origine les multiples calculs redondants. Mettre en œuvre la méthode de mémoïsation afin d’améliorer l’efficacité du code de la fonction nb_rendre.
defnb_rendre(a_rendre:int,pieces:List[int])->int:"""
Détermine le nombre minimal de pièces
à rendre.
Algorithme récursif avec mémoïsation.
"""dic_a_rendre={0:0}def_nb_rendre(a_rendre:int,pieces:List[int],dic:Dict[int,int])->int:"""
Version mémoïsée de la fonction nb_rendre.
"""ifa_rendrenotindic.keys():nb_min=float('inf')foriinrange(len(pieces)):ifpieces[i]<=a_rendre:nb=1+_nb_rendre(a_rendre-pieces[i],pieces,dic)ifnb<=nb_min:nb_min=nbdic[a_rendre]=nb_minreturndic[a_rendre]return_nb_rendre(a_rendre,pieces,dic_a_rendre)
Rappeler quel est l’objectif de la programmation dynamique.
Réponse
La programmation dynamique, comme la méthode diviser-pour-régner, résout des problèmes en combinant des solutions de sous-problèmes.
Les algorithmes diviser-pour-régner partitionnent le problème en sous-problèmes indépendants qu’ils résolvent récursivement, puis combinent leurs solutions pour résoudre le problème initial.
La programmation dynamique, quant à elle, peut s’appliquer même lorsque les sous-problèmes ne sont pas indépendants, c’est-à-dire lorsque des sous-problèmes ont des sous-sous-problèmes communs.
Dans ce cas, un algorithme diviser-pour-régner fait plus de travail que nécessaire, en résolvant plusieurs fois le sous-sous-problème commun. Un algorithme de programmation dynamique résout chaque sous-sous-problème une seule fois et mémorise sa réponse dans un tableau, évitant ainsi le recalcul de la solution chaque fois que le sous-sous-problème est rencontré.
La programmation dynamique est, en général, appliquée aux problèmes d’optimisation. Dans ce type de problèmes, il peut y avoir de nombreuses solutions possibles. Chaque solution a une valeur, et on souhaite trouver une solution ayant la valeur optimale (minimale ou maximale). Une telle solution est une solution optimale au problème, et non pas la solution optimale, puisqu’il peut y avoir plusieurs solutions qui donnent la valeur optimale.
Le manque d’efficacité de la méthode force brute a pour origine les multiples calculs redondants. Mettre en œuvre la méthode de ascendante afin d’améliorer l’efficacité du code de la fonction nb_rendre.
Réponse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defnb_rendre(a_rendre:int,pieces:List[int])->int:"""
Détermine le nombre minimal de pièces
à rendre.
Algorithme itératif, méthode ascendante.
"""nbres=[float('inf')]*(a_rendre+1)# Tableau des valeursnbres[0]=0forsommeinrange(1,len(nbres)):forpieceinpieces:if(somme-piece)>=0:nbres[somme]=min(nbres[sommes],1+nbres[somme-piece])returnnbres[a_rendre]
En résumé
Rendu de monnaie - Nombre de pièces à rendre - Toutes les méthodes
"""
Les mille et unes façon de rendre la monnaie.
"""fromtypingimportList,Dictdefglouton(somme:int,pieces:List[int])->int:"""
Retourne le nombre de pièces nécessaires pour
rendre la monnaie pour somme.
Méthode gloutonne, pièces est censé être trié
dans le sens décroissant.
"""nbre=0# nombre de pièces à rendreforpieceinpieces:nbre+=somme//piecesomme=somme%piecereturnnbredefforce_brute(somme:int,pieces:List[int])->int:"""
Retourne le nombre de pièces nécessaires pour
rendre la monnaie pour somme.
Méthode force brute.
"""ifsomme==0:return0nbre_min=float('inf')forpieceinpieces:ifsomme-piece>=0:nbre_new=1+force_brute(somme-piece,pieces)ifnbre_new<nbre_min:nbre_min=nbre_newreturnnbre_mindefmemoisation(somme:int,pieces:List[int],memo:Dict[int,int]={0:0})->int:"""
Retourne le nombre de pièces nécessaires pour
rendre la monnaie pour somme.
Programmation dynamique : mémoïsation.
"""ifsommenotinmemo:nbre_min=float('inf')forpieceinpieces:ifsomme-piece>=0:nbre_new=1+memoisation(somme-piece,pieces,memo)ifnbre_new<nbre_min:nbre_min=nbre_newmemo[somme]=nbre_minreturnmemo[somme]deftabulaire(somme:int,pieces:List[int])->int:"""
Retourne le nombre de pièces nécessaires pour
rendre la monnaie pour somme.
Programmation dynamique : méthode tabulaire.
Fonction enveloppe.
"""nbre_pour_somme=[float('inf')foriinrange(somme+1)]nbre_pour_somme[0]=0def_tabulaire(somme:int,pieces:List[int],nbre_pour_somme):"""
Mise en œuvre de la méthode tabulaire.
"""forsomme_a_rendreinrange(1,somme+1):forpieceinpieces:ifsomme_a_rendre-piece>=0:nbre=1+nbre_pour_somme[somme_a_rendre-piece]ifnbre<nbre_pour_somme[somme_a_rendre]:nbre_pour_somme[somme_a_rendre]=nbrereturnnbre_pour_somme[somme]return_tabulaire(somme,pieces,nbre_pour_somme)if__name__=="__main__":somme_a_rendre=60pieces=[25,20,1]nbres_pieces=glouton(somme_a_rendre,pieces)print(f"Méthode gloutonne pour rendre {somme_a_rendre} : {nbres_pieces} pieces.")nbres_pieces=force_brute(somme_a_rendre,pieces)print(f"Méthode force-brute pour rendre {somme_a_rendre} : {nbres_pieces} pieces.")nbres_pieces=memoisation(somme_a_rendre,pieces)print(f"Méthode mémoïsation pour rendre {somme_a_rendre} : {nbres_pieces} pieces.")nbres_pieces=tabulaire(somme_a_rendre,pieces)print(f"Méthode tabulaire pour rendre {somme_a_rendre} : {nbres_pieces} pieces.")