Dans ce TD nous allons aborder le problème de rendu de monnaie selon plusieurs méthodes algorithmiques :
Dans ce TD nous allons aborder le problème de rendu de monnaie selon plusieurs méthodes algorithmiques :
- technique dite Gloutonne
- technique dite Gloutonne
- chemin minimal dans un arbre de recherche
- chemin minimal dans un arbre de recherche
- Programmation Dynamique (devoir à rendre)
- Programmation Dynamique (devoir à rendre)
Vous serez amené également à créer une structure de données de graphe et une méthode de parcours de celui-ci.
Vous serez amené également à créer une structure de données de graphe et une méthode de parcours de celui-ci.
**Les réponses de la partie sur la programmation dynamique feront l'objet d'un autre sujet de TD et d'un rendu sous forme de rapport à rendre sur Moodle.**
**Les réponses de la partie sur la programmation dynamique feront l'objet d'un autre sujet de TD et d'un rendu sous forme de rapport à rendre sur Moodle.**
Le problème de rendu de monnaie est très fréquent dans la vie de tous les jours et peut être défini comme suit : étant donné un montant, une machine capable de rendre la monnaie doit rendre ce montant au client à partir de pièces (1c à 2€) et de billets. On suppose pour simplifier qu'il n'y a que des pièces en centimes; un billet de 5€ sera représenté comme une pièce de 500 centimes. On supposera dans un premier temps qu'il existe un nombre suffisant (autant que nécessaire) de chaque pièce, mais dans un second temps nous introduirons des contraintes de disponibilité des pièces.
Le problème de rendu de monnaie est très fréquent dans la vie de tous les jours et peut être défini comme suit : étant donné un montant, une machine capable de rendre la monnaie doit rendre ce montant au client à partir de pièces (1c à 2€) et de billets. On suppose pour simplifier qu'il n'y a que des pièces en centimes; un billet de 5€ sera représenté comme une pièce de 500 centimes. On supposera dans un premier temps qu'il existe un nombre suffisant (autant que nécessaire) de chaque pièce, mais dans un second temps nous introduirons des contraintes de disponibilité des pièces.
De manière plus formelle, un stock de pièces est un tuple $S=(v_1, v_2, ..., v_n)$ où l'entier $v_i > 0$ est la valeur de la $i^{ème}$ pièce. Pour refléter le fait qu'on a des pièces de 1, 2 et 5c, $S$ contiendra $v_1=1$ (1 centime), $v_2=2, v_3=5$. Le problème de monnaie est un problème d'optimisation combinatoire $(S,M)$ permettant de trouver le tuple $T=(x_1, x_2, ..., x_n)$ avec $x_i \geq 0$ qui minimise $ \sum_{i=1}^n x_i$ sous la contrainte $\sum_{i=1}^n x_i.v_i=M$. Autrement dit, nous souhaitons aussi bien obtenir le montant exact, que minimiser le nombre total de pièces $x_i$ de valeur $v_i$ utilisées. Appelons $Q(S,M) = \sum_{i=1}^n x_i$ la quantité de pièces à rendre pour le montant *M* étant donné le système *S* décrit dans la Table~1. Une solution optimale $Q_{opt}$ à ce problème est telle que *Q(S,M)* soit minimale :
De manière plus formelle, un stock de pièces est un tuple $S=(v_1, v_2, ..., v_n)$ où l'entier $v_i > 0$ est la valeur de la $i^{ème}$ pièce. Pour refléter le fait qu'on a des pièces de 1, 2 et 5c, $S$ contiendra $v_1=1$ (1 centime), $v_2=2, v_3=5$. Le problème de monnaie est un problème d'optimisation combinatoire $(S,M)$ permettant de trouver le tuple $T=(x_1, x_2, ..., x_n)$ avec $x_i \geq 0$ qui minimise $ \sum_{i=1}^n x_i$ sous la contrainte $\sum_{i=1}^n x_i.v_i=M$. Autrement dit, nous souhaitons aussi bien obtenir le montant exact, que minimiser le nombre total de pièces $x_i$ de valeur $v_i$ utilisées. Appelons $Q(S,M) = \sum_{i=1}^n x_i$ la quantité de pièces à rendre pour le montant *M* étant donné le système *S* décrit dans la Table~1. Une solution optimale $Q_{opt}$ à ce problème est telle que *Q(S,M)* soit minimale :
$Q_{opt}(S,M) = min \ \sum_{i=1}^n x_i$.
$Q_{opt}(S,M) = min \ \sum_{i=1}^n x_i$.
Dans certaines situations il faudra gérer le nombre de pièces/billets disponibles (la table *D*). Nous noterons *d[i]=k* pour dire : il y a *k* pièces/billets du montant *$v_i$* disponibles (pièces ou billets du montant *v[i]*) à l'indice *i* dans la table *S*. On supposera cependant dans un premier temps qu'il y a un nombre suffisant de chaque pièce/billet dans le tableau S. On supposera également que *S* est ordonné dans un ordre croissant.
Dans certaines situations il faudra gérer le nombre de pièces/billets disponibles (la table *D*). Nous noterons *d[i]=k* pour dire : il y a *k* pièces/billets du montant *$v_i$* disponibles (pièces ou billets du montant *v[i]*) à l'indice *i* dans la table *S*. On supposera cependant dans un premier temps qu'il y a un nombre suffisant de chaque pièce/billet dans le tableau S. On supposera également que *S* est ordonné dans un ordre croissant.
**M = 9€:** étant donné S dans la Table ci-dessous, la solution qui minimise le nombre total de pièces rendues à 3 est *T=(0,0,0,0,0,0,0,2,1,0,0,0,0)*. Donc, $Q_{opt}(S,9) = \min \ Q(S,9) = 3$. Détails (avec des pièces $\geq$ 1€) :
**M = 9€:** étant donné S dans la Table ci-dessous, la solution qui minimise le nombre total de pièces rendues à 3 est *T=(0,0,0,0,0,0,0,2,1,0,0,0,0)*. Donc, $Q_{opt}(S,9) = \min \ Q(S,9) = 3$. Détails (avec des pièces $\geq$ 1€) :
L'exemple de la Table 1 est un système *canonique*, c'est à dire qu'en choisissant systématiquement les pièces de plus grande valeur (algorithme glouton) on obtient toujours la solution optimale. Il existe des systèmes pour lesquels c'est moins simple, par exemple $S=(1,7,23)$. Pour **M = 28**, en choisissant en priorité les pièces de plus grande valeur on trouvera $T=(5,0,1)$ (6 pièces), alors que la solution optimale est $T=(0,4,0)$ (4 pièces).
L'exemple de la Table 1 est un système *canonique*, c'est à dire qu'en choisissant systématiquement les pièces de plus grande valeur (algorithme glouton) on obtient toujours la solution optimale. Il existe des systèmes pour lesquels c'est moins simple, par exemple $S=(1,7,23)$. Pour **M = 28**, en choisissant en priorité les pièces de plus grande valeur on trouvera $T=(5,0,1)$ (6 pièces), alors que la solution optimale est $T=(0,4,0)$ (4 pièces).
Dans le cas général, ce problème est démontré NP-difficile, c'est-à-dire qu'on ne connaît pas d'algorithme qui puisse le résoudre en complexité polynomiale par rapport à la taille de $S$.
Dans le cas général, ce problème est démontré NP-difficile, c'est-à-dire qu'on ne connaît pas d'algorithme qui puisse le résoudre en complexité polynomiale par rapport à la taille de $S$.
Il existe plusieurs approches :
Il existe plusieurs approches :
1. Approche gloutonne (pas toujours optimale)
1. Approche gloutonne (pas toujours optimale)
2. Recherche de chemin de longueur minimale avec (ou sans) un arbre de recherche, ...
2. Recherche de chemin de longueur minimale avec (ou sans) un arbre de recherche, ...
3. Programmation Dynamique, ... et autres méthodes algorithmiques (cf. quasi-Dijkstra)
3. Programmation Dynamique, ... et autres méthodes algorithmiques (cf. quasi-Dijkstra)
Il existe d'autres approches de résolutions algorithmiques ou non-algorithmiques comme par exemple un système d'équations à optimiser. Nous allons dans ce TD aborder les différentes approches algorithmiques mentionnées ci-dessus.
Il existe d'autres approches de résolutions algorithmiques ou non-algorithmiques comme par exemple un système d'équations à optimiser. Nous allons dans ce TD aborder les différentes approches algorithmiques mentionnées ci-dessus.
Les techniques de programmation gloutonnes ont la particularité de faire des choix locaux à chaque étape de calcul. Cette méthode ne donne pas forcément un nombre minimal de pièces mais elle est simple à comprendre et à implémenter. Son application au problème de rendu de monnaie est simple : on trouve la pièce la plus grande inférieure ou égale à $M$. Soit $v_i$ cette pièce. On utilise ($x_i = M \ div \ v_i$) fois la pièce $v_i$; le reste à traiter sera $M'=(M \mod v_i)$. Ensuite, on recommence suivant le même principe pour satisfaire $M'$. Ce qui donne le pseudo-code d'algorithme suivant (qui fait l'hypothèse d'un nombre illimité de pièces, on ne tient pas compte ici de $D$ : $d_i=\infty$) :
Les techniques de programmation gloutonnes ont la particularité de faire des choix locaux à chaque étape de calcul. Cette méthode ne donne pas forcément un nombre minimal de pièces mais elle est simple à comprendre et à implémenter. Son application au problème de rendu de monnaie est simple : on trouve la pièce la plus grande inférieure ou égale à $M$. Soit $v_i$ cette pièce. On utilise ($x_i = M \ div \ v_i$) fois la pièce $v_i$; le reste à traiter sera $M'=(M \mod v_i)$. Ensuite, on recommence suivant le même principe pour satisfaire $M'$. Ce qui donne le pseudo-code d'algorithme suivant (qui fait l'hypothèse d'un nombre illimité de pièces, on ne tient pas compte ici de $D$ : $d_i=\infty$) :
```
```
Fonction Monnaie_Gloutonne
Fonction Monnaie_Gloutonne
Entrées : la somme S, M
Entrées : la somme S, M
Sorties : le vecteur T = Q(S,M) : le nombre de pièces nécessaires
Sorties : le vecteur T = Q(S,M) : le nombre de pièces nécessaires
M'=M
M'=M
Répéter
Répéter
Chercher dans S l'indice i tel que Vi =< M'
Chercher dans S l'indice i tel que Vi =< M'
Ti = M' div Vi
Ti = M' div Vi
M' = M' mod Si
M' = M' mod Si
Jusqu'à M' = 0
Jusqu'à M' = 0
Constituer T avec les Ti utilisés
Constituer T avec les Ti utilisés
$Q(S,M) = somme de i=1 a i=n de T_i$
Q(S,M) = somme de i=1 a i=n de T_i
T est la valeur de sortie de l'algorithme
T est la valeur de sortie de l'algorithme
Fin Monnaie_Gloutonne
Fin Monnaie_Gloutonne
```
```
Pour **M = 236,65€** cet algorithme se déroule de la manière suivante :
Pour **M = 236,65€** cet algorithme se déroule de la manière suivante :
$23665 \geq 10000$
$23665 \geq 10000$
- $T_{13}=23665 \ div \ 10000 = 2$ ($T_{13}$ correspond à 100€)
- $T_{13}=23665 \ div \ 10000 = 2$ ($T_{13}$ correspond à 100€)
- $M'=23665 \ mod \ 10000 = 3665$
- $M'=23665 \ mod \ 10000 = 3665$
$3665 \geq 2000$
$3665 \geq 2000$
- $T_{11}=3665 \ div \ 2000 = 1$
- $T_{11}=3665 \ div \ 2000 = 1$
- $M'=3665 \ mod \ 2000 = 1665$
- $M'=3665 \ mod \ 2000 = 1665$
...
...
$\rightarrow$ On obtient $T_{1..13}=(0,0,1,1,0,1,1,0,1,1,1,0,2)$ et $Q(S,M) = 9$
$\rightarrow$ On obtient $T_{1..13}=(0,0,1,1,0,1,1,0,1,1,1,0,2)$ et $Q(S,M) = 9$
A noter qu'en Python les indices commencent à zéro ! Faire -1 sur les indices ci-dessus.
A noter qu'en Python les indices commencent à zéro ! Faire -1 sur les indices ci-dessus.
**Question 1.2 -** Proposez une modification du pseudo-code ci-dessus pour prendre en compte un nombre **limité** de pièces (en prenant en compte la table $D$). Implémenter cet algorithme modifié (vous fournirez vos propres valeurs de disponibilité dans $D$).
**Question 1.2 -** Proposez une modification du pseudo-code ci-dessus pour prendre en compte un nombre **limité** de pièces (en prenant en compte la table $D$). Implémenter cet algorithme modifié (vous fournirez vos propres valeurs de disponibilité dans $D$).
Vous noterez que la méthode Gloutonne ne garantit pas que *Q(S, M)* soit minimal comme les tests l'ont montré pour M=28 et S=(1, 7, 23). La méthode Gloutonne utilise un optimum local (choix de la plus grande pièce) qui ne débouche pas forcément sur un optimum global. Cependant, elle est très simple à calculer.
Vous noterez que la méthode Gloutonne ne garantit pas que *Q(S, M)* soit minimal comme les tests l'ont montré pour M=28 et S=(1, 7, 23). La méthode Gloutonne utilise un optimum local (choix de la plus grande pièce) qui ne débouche pas forcément sur un optimum global. Cependant, elle est très simple à calculer.
Une deuxième méthode consiste à construire toutes les solutions possibles sous forme d'arbre, et ensuite de choisir la solution de manière globale. Cette méthode est plus complexe à mettre en oeuvre mais donne une solution optimale.
Une deuxième méthode consiste à construire toutes les solutions possibles sous forme d'arbre, et ensuite de choisir la solution de manière globale. Cette méthode est plus complexe à mettre en oeuvre mais donne une solution optimale.
Exemple d'arbre de recherche résultant *M=28* et *S=(1,7,23)*. C'est à dire, on a seulement des pièces de 1, 7 et 23 (€ ou cents) en nombre suffisant. Dès qu'on atteint 0, on remonte de ce 0 à la racine pour obtenir la solution minimale donnant le nombre minimal d'arcs entre ce 0 et la racine. On remarque qu'on utilisera 4 pièces de 7c pour avoir 28c. Par ailleurs, on remarque que le choix initial de 23 (à gauche de l'arbre) laisse le montant *M'=5c* à satisfaire lequel impose le choix de 5 pièces de 1c. En bleu les autres chemins choisissant une pièce de 23.
Exemple d'arbre de recherche résultant *M=28* et *S=(1,7,23)*. C'est à dire, on a seulement des pièces de 1, 7 et 23 (€ ou cents) en nombre suffisant. Dès qu'on atteint 0, on remonte de ce 0 à la racine pour obtenir la solution minimale donnant le nombre minimal d'arcs entre ce 0 et la racine. On remarque qu'on utilisera 4 pièces de 7c pour avoir 28c. Par ailleurs, on remarque que le choix initial de 23 (à gauche de l'arbre) laisse le montant *M'=5c* à satisfaire lequel impose le choix de 5 pièces de 1c. En bleu les autres chemins choisissant une pièce de 23.
On construit un arbre de recherche (arbre des possibilités) dont la racine est *M*. Chaque noeud de l'arbre représente un montant : les noeuds autres que la racine initiale représentent $M' < M$ une fois qu'on aura utilisé une (seule) des pièces de $S$ (parmi 1, 7 ou 23€). La pièce utilisée pour aller de $M$ à $M'$ sera la valeur de l'arc reliant ces deux montants. Cet arbre est donc développé par niveau (en largeur). On arrête de développer un niveau supplémentaire dès qu'un noeud a atteint 0 auquel cas une solution sera le vecteur des valeurs (des arcs) allant de la racine à ce noeud.
On construit un arbre de recherche (arbre des possibilités) dont la racine est *M*. Chaque noeud de l'arbre représente un montant : les noeuds autres que la racine initiale représentent $M' < M$ une fois qu'on aura utilisé une (seule) des pièces de $S$ (parmi 1, 7 ou 23€). La pièce utilisée pour aller de $M$ à $M'$ sera la valeur de l'arc reliant ces deux montants. Cet arbre est donc développé par niveau (en largeur). On arrête de développer un niveau supplémentaire dès qu'un noeud a atteint 0 auquel cas une solution sera le vecteur des valeurs (des arcs) allant de la racine à ce noeud.
Le principe de l'algorithme correspondant à un parcours en largeur dont la version itérative utilise une *File d'attente* est :
Le principe de l'algorithme correspondant à un parcours en largeur dont la version itérative utilise une *File d'attente* est :
```
```
Fonction Monnaie_graphe
Fonction Monnaie_graphe
% on suppose qu'il y a un nombre suffisant de chaque pièce/billet dans la tableau S
% on suppose qu'il y a un nombre suffisant de chaque pièce/billet dans la tableau S
Entrées : la somme M
Entrées : la somme M
Sorties : le vecteur T et Qopt(S,M) le nombre de pièces nécessaires
Sorties : le vecteur T et Qopt(S,M) le nombre de pièces nécessaires
File F = vide ;
File F = vide ;
Arbre A contenant un noeud racine dont la valeur = M
Arbre A contenant un noeud racine dont la valeur = M
Enfiler(M) % la file F contient initialement M
Enfiler(M) % la file F contient initialement M
Répéter
Répéter
M'= défiler()
M'= défiler()
Pour chaque pièce vi =< M' disponible dans S :
Pour chaque pièce vi =< M' disponible dans S :
S'il existe dans l'arbre A un noeud dont la valeur est M' - vi
S'il existe dans l'arbre A un noeud dont la valeur est M' - vi
Alors établir un arc étiqueté par vi allant de M' à ce noeud
Alors établir un arc étiqueté par vi allant de M' à ce noeud
Sinon
Sinon
Créer un nouveau noeud de valeur M' - vi dans A et lier ce noeud
Créer un nouveau noeud de valeur M' - vi dans A et lier ce noeud
à M' par un arc étiqueté par vi
à M' par un arc étiqueté par vi
Enfiler ce nouveau noeud
Enfiler ce nouveau noeud
Fin Si
Fin Si
Jusqu'à (M' - vi = 0) ou (F = vide)
Jusqu'à (M' - vi = 0) ou (F = vide)
Si (F est vide Et M' - vi /= 0)
Si (F est vide Et M' - vi /= 0)
Alors il y a un problème dans les calculs ! STOP.
Alors il y a un problème dans les calculs ! STOP.
Sinon Le dernier noeud créer porte la valeur 0
Sinon Le dernier noeud créer porte la valeur 0
On remonte de ce noeud à la racine et on comptabilise dans T où
On remonte de ce noeud à la racine et on comptabilise dans T où
Ti = le nombre d'occurrences des arcs étiquetés vi de la racine
Ti = le nombre d'occurrences des arcs étiquetés vi de la racine
Vous devriez pouvoir visualiser votre arbre avec la méthode `plot_graph` (chargez d'abord les dernières cellules du notebook afin d'initialiser cette fonction) :
Vous devriez pouvoir visualiser votre arbre avec la méthode `plot_graph` (chargez d'abord les dernières cellules du notebook afin d'initialiser cette fonction) :
A noter que vous pouvez combiner la construction et la recherche du plus court chemin (en vous arrêtant au premier 0 rencontré). Cependant, cette methode a pour inconvénient d'explorer un très large espace de recherche (qui devient vite très grand). Un autre inconvénient est le calcul de solutions dont on sait qu'elles ne seront pas optimales.
A noter que vous pouvez combiner la construction et la recherche du plus court chemin (en vous arrêtant au premier 0 rencontré). Cependant, cette methode a pour inconvénient d'explorer un très large espace de recherche (qui devient vite très grand). Un autre inconvénient est le calcul de solutions dont on sait qu'elles ne seront pas optimales.