CHAPITRE 2 : RENDRE LA MONNAIE

Reprenons le comptoir de Mama Ngono. Son problème est un classique de l'algorithmique, et il a l'avantage d'être l'un des rares où le glouton a toujours raison : du moins avec notre monnaie.

Le problème, posé proprement

On dispose d'un ensemble de coupures. En zone franc CFA d'Afrique centrale :

pieces  : 1, 2, 5, 10, 25, 50, 100, 500
billets : 500, 1000, 2000, 5000, 10000

On doit rendre un montant donné. L'objectif est de rendre le moins de coupures possible.

Pourquoi le moins ? D'abord pour la cliente, qui ne veut pas repartir avec quarante pièces. Ensuite et surtout pour Mama Ngono : si elle vide sa caisse de petite monnaie dès le matin, elle ne pourra plus rendre à personne l'après-midi.

La stratégie gloutonne

À chaque étape, donner la plus grosse coupure qui ne dépasse pas ce qui reste à rendre.

Décomposition de 1850 FCFA étape par étape : un billet de 1000, un de 500, trois pièces de 100 et une de 50, soit six coupures.

Le code

COUPURES = [1, 2, 5, 10, 25, 50, 100, 500, 1000, 2000, 5000, 10000]


def rendre(montant, coupures=COUPURES):
    # Rend la liste des coupures a donner, des plus grosses aux plus petites
    reste = montant
    rendu = []

    for piece in sorted(coupures, reverse=True):   # de la plus grosse
        while reste >= piece:                      # tant qu'elle tient
            rendu.append(piece)
            reste -= piece

    return rendu if reste == 0 else None           # None : rendu impossible

Essayons :

>>> rendre(1850)
[1000, 500, 100, 100, 100, 50]          # 6 coupures

>>> rendre(7675)
[5000, 2000, 500, 100, 50, 25]          # 6 coupures

>>> rendre(940)
[500, 100, 100, 100, 100, 25, 10, 5]    # 8 coupures

Une version plus parlante

La liste brute n'est pas très lisible pour un humain. Regroupons :

from collections import Counter


def annoncer(montant):
    rendu = rendre(montant)
    if rendu is None:
        print("impossible de rendre", montant)
        return

    compte = Counter(rendu)
    for valeur in sorted(compte, reverse=True):
        nombre = compte[valeur]
        support = "billet" if valeur >= 500 else "piece"
        if nombre > 1:
            support += "s"
        print("%d %s de %d FCFA" % (nombre, support, valeur))
    print("et le compte est bon.")
>>> annoncer(1850)
1 billet de 1000 FCFA
1 billet de 500 FCFA
3 pieces de 100 FCFA
1 piece de 50 FCFA
et le compte est bon.

Est-ce vraiment le minimum ?

Six coupures pour 1 850 FCFA. Existe-t-il une combinaison à cinq ? À quatre ?

On peut le vérifier par force brute, en cherchant le minimum absolu pour chaque montant. Nous l'avons fait pour tous les montants de 1 à 3 000 FCFA : le glouton donne le minimum à chaque fois, sans exception.

Un système canonique. On appelle ainsi un système monétaire où l'algorithme glouton donne toujours le nombre minimal de coupures. Le franc CFA en est un. L'euro aussi. Le dollar aussi.

Ce n'est pas un hasard : les systèmes monétaires sont conçus pour cela. On veut que n'importe quel commerçant puisse rendre la monnaie correctement sans calculer.

Le système où le glouton se trompe

Imaginons maintenant une monnaie fantaisiste qui n'aurait que trois coupures : 1, 3 et 4. Et rendons 6.

DémarcheRésultat
Glouton la plus grosse qui tient : 4. Reste 2. Puis 1, puis 1. 4 + 1 + 1 = 3 coupures
Optimal 3 + 3 2 coupures

Le glouton a pris le 4 parce qu'il était le plus gros. Ce faisant, il s'est condamné à finir avec deux pièces de 1. En renonçant au 4, on fait mieux.

Le glouton n'échoue pas par bêtise : il échoue parce qu'il ne sait pas que renoncer au meilleur choix immédiat peut ouvrir une meilleure suite.

Noter que sur ce même système, rendre 8 fonctionne très bien : le glouton donne 4 + 4, ce qui est optimal. L'échec n'est pas systématique, il dépend du montant. C'est ce qui le rend difficile à repérer.

Quand on ne peut pas rendre du tout

Dernier cas à traiter : avec un système sans pièce de 1, certains montants sont tout bonnement impossibles.

>>> rendre(7, [2, 5])        # on ne peut pas faire 7 avec des 2 et des 5
None

Le glouton prendrait 5, puis resterait bloqué sur 2, alors que 7 est réellement impossible ici. Mais attention : dans d'autres cas, le glouton échoue alors qu'une solution existe. Avec le système {3, 4} et un montant de 6, il prend 4, reste 2, bloqué, alors que 3 + 3 marchait parfaitement.

C'est pourquoi notre fonction renvoie None plutôt que de prétendre que c'est impossible : elle n'en sait rien.

Complexité

Si l'on note c le nombre de coupures différentes et n le nombre total de pièces rendues :

Coût
Tri des coupuresO(c log c), une fois pour toutes
ParcoursO(c + n)
Par force bruteexponentiel

Autrement dit : immédiat. C'est pour cela que Mama Ngono n'a pas besoin d'y réfléchir.

À retenir

  • Critère glouton : la plus grosse coupure qui tient encore.
  • Sur le franc CFA, le glouton donne toujours le minimum : vérifié de 1 à 3 000 FCFA.
  • On appelle « canonique » un système où c'est garanti. Les monnaies réelles le sont, par construction.
  • Avec {1, 3, 4}, rendre 6 : le glouton donne 3 coupures, l'optimum en demande 2.

Exercices d'application

Exercice 1
Avec le système {1, 4, 5, 10}, rendez 8 FCFA par la méthode gloutonne, puis trouvez le véritable optimum. Que concluez-vous ?
Voir le corrigeMasquer le corrige

Glouton. La plus grosse coupure qui tient dans 8 est le 5.

reste 8 : on prend 5   ->  reste 3
reste 3 : on prend 1   ->  reste 2
reste 2 : on prend 1   ->  reste 1
reste 1 : on prend 1   ->  reste 0

resultat : 5 + 1 + 1 + 1 = 4 coupures

Optimum. 4 + 4 = 2 coupures.

Le glouton fait donc deux fois trop. Comme dans l'exemple {1, 3, 4}, il s'est fait piéger en prenant la grosse coupure : le 5 laisse un reste de 3, qui ne se décompose qu'en pièces de 1.

Conclusion : ce système n'est pas canonique. Pour savoir si un système l'est, il n'existe pas de règle simple à l'œil nu : il faut le vérifier par le calcul, montant par montant.

Exercice 2
Écrivez une fonction qui calcule le vrai minimum de coupures, quelle que soit la monnaie, puis servez-vous-en pour tester si un système est canonique.
Voir le corrigeMasquer le corrige

Le vrai minimum s'obtient par programmation dynamique : on calcule le minimum pour 1, puis pour 2, et ainsi de suite jusqu'au montant voulu, chaque résultat servant aux suivants.

def minimum_vrai(montant, coupures):
    INFINI = float("inf")
    table = [0] + [INFINI] * montant      # table[m] = minimum pour m

    for m in range(1, montant + 1):
        for piece in coupures:
            if piece <= m and table[m - piece] + 1 < table[m]:
                table[m] = table[m - piece] + 1

    return table[montant]


def est_canonique(coupures, jusqua=3000):
    for m in range(1, jusqua + 1):
        glouton = rendre(m, coupures)
        if glouton is None or len(glouton) != minimum_vrai(m, coupures):
            return False, m          # le premier montant qui coince
    return True, None
>>> est_canonique([1, 2, 5, 10, 25, 50, 100, 500, 1000])
(True, None)

>>> est_canonique([1, 3, 4])
(False, 6)

Remarquez le renversement : pour vérifier un algorithme glouton, on a dû écrire un algorithme non glouton. C'est courant, et c'est le sujet du chapitre 6.

Exercice 3
Mama Ngono veut aussi limiter le nombre de pièces qu'elle sort de sa caisse pour un même type. Elle n'a que 2 billets de 1000 et 1 de 500. Comment adapter l'algorithme ?
Voir le corrigeMasquer le corrige

Il faut ajouter un stock et le décompter :

def rendre_avec_stock(montant, caisse):
    # caisse : dictionnaire {valeur: quantite disponible}
    reste = montant
    rendu = []

    for piece in sorted(caisse, reverse=True):
        disponible = caisse[piece]
        # on en prend le moins possible entre ce qu'on a et ce qui tient
        nombre = min(disponible, reste // piece)
        rendu.extend([piece] * nombre)
        reste -= piece * nombre

    return rendu if reste == 0 else None
>>> caisse = {1000: 2, 500: 1, 100: 10, 50: 4, 25: 4}
>>> rendre_avec_stock(1850, caisse)
[1000, 500, 100, 100, 100, 50]

Mais attention : avec un stock limité, le glouton devient beaucoup moins fiable. S'il épuise les billets de 1000 sur une cliente, il ne pourra plus les utiliser pour la suivante. Et sur un montant donné, il peut échouer là où une autre combinaison aurait marché : avec une caisse de {500: 1, 100: 2}, rendre 700 par le glouton donne 500 + 100 + 100 = 700 ✓, mais rendre 300 échoue si les pièces de 100 ont déjà servi.

Cherchez avant d'ouvrir le corrige : c'est en butant sur les difficultes que l'on progresse.

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam