Aller au contenu principal

Gloutons et programmation dynamique

Ce que ce chapitre apporte 5 points
  • Décrire la stratégie gloutonne et reconnaître les décisions qu'elle prend.
  • Construire un contre-exemple qui met un glouton en défaut.
  • Mesurer au compteur de pas l'explosion d'un calcul récursif qui se répète.
  • Expliquer ce que retenir un résultat déjà calculé fait gagner.
  • Remplir un tableau de proche en proche pour obtenir un optimum garanti.

Un distributeur de consommables, à l'entrée d'un atelier, rend la monnaie avec les pièces de ses tubes, et chaque pièce rendue en trop vide un tube plus vite et rapproche le prochain rechargement. Rendre le moins de pièces possible est un problème d'optimisation, et devant un tel problème, la première idée est toujours la même : prendre à chaque étape ce qui paraît le mieux sur le moment, et ne jamais revenir dessus. C'est la stratégie gloutonne. Elle est rapide, elle est simple à écrire, et elle donne parfois la meilleure solution possible. Reste à savoir quand, et ce qu'il faut faire quand la réponse est non. Le chapitre s'appuie sur la récursivité, sur le compteur de pas employé depuis les tris, et sur le contre-exemple et l'invariant de prouver un algorithme.

Rendre la monnaie avec le moins de pièces possible, remplir un sac sans dépasser son poids, choisir les réunions qui tiennent dans une journée : ces problèmes ont en commun de demander non pas une solution, mais la meilleure. Le nombre de solutions candidates y grandit vite, et les examiner toutes devient impossible bien avant que les données ne soient grosses.

Deux familles de méthodes répondent à cette difficulté sans tout énumérer. La première décide vite et ne revient jamais en arrière. La seconde accepte de calculer davantage, mais refuse de calculer deux fois la même chose. Le rendu de monnaie suffit à montrer l'une et l'autre, et il sert de fil au chapitre entier.

Le glouton : prendre le mieux tout de suite

Définition

Un algorithme glouton construit une solution par étapes. À chaque étape, il choisit ce qui paraît le meilleur immédiatement, selon un critère local, et ne remet jamais ce choix en question.

Le rendu de monnaie est l'exemple canonique. Il s'agit de payer une somme avec le moins de pièces possible, en piochant dans un jeu de valeurs disponibles. Le critère local est immédiat : prendre à chaque fois la plus grosse pièce qui ne dépasse pas ce qu'il reste à rendre.

Algorithme
pas 1 / 37
Début
pieces ← [50, 20, 10, 5, 2, 1]
somme ← 68
nombre ← 0
Pour i de 1 à longueur(pieces)
TantQue somme >= pieces[i]
somme ← somme - pieces[i]
nombre ← nombre + 1
Écrire "pièce de ", pieces[i], " -> reste ", somme
FinTantQue
FinPour
Écrire "total : ", nombre, " pièces"
Fin

programme principal

pieces[50, 20, 10, 5, 2, 1]

Le déroulement tient en trente-sept pas et produit la suite 50, 10, 5, 2, 1, soit cinq pièces pour 68 centimes. Le critère local tient dans l'ordre du tableau : les pièces y sont rangées de la plus grosse à la plus petite, et c'est cet ordre qui fait essayer d'abord la plus grosse. Aucune combinaison ne fait mieux, et ce n'est pas un hasard : sur le système de pièces européen, le glouton donne toujours l'optimum. La propriété se démontre pour ce jeu précis, et elle n'a rien d'automatique : l'exercice 1 présente un jeu aux valeurs bien espacées où elle est fausse. Elle explique pourtant pourquoi personne n'a jamais eu à réfléchir en rendant la monnaie.

Ce qui rend un glouton attirant

Il ne garde presque rien en mémoire : la solution partielle et ce qu'il reste à traiter. Il ne revient jamais en arrière, donc son coût est celui d'un simple parcours. Et son critère local tient en une phrase, ce qui rend l'algorithme court et lisible.

Vérification rapideon peut se reprendre

Qu'est-ce qui caractérise un algorithme glouton ?

Le même glouton, mis en échec

La phrase « le glouton donne l'optimum » portait sur un jeu de pièces précis. Il suffit de changer le jeu de pièces pour que la propriété disparaisse. Voici le même algorithme, mot pour mot, sur un système à trois valeurs : 4, 3 et 1.

Algorithme
pas 1 / 23
Début
pieces ← [4, 3, 1]
somme ← 6
nombre ← 0
Pour i de 1 à longueur(pieces)
TantQue somme >= pieces[i]
somme ← somme - pieces[i]
nombre ← nombre + 1
Écrire "pièce de ", pieces[i], " -> reste ", somme
FinTantQue
FinPour
Écrire "total : ", nombre, " pièces"
Fin

programme principal

pieces[4, 3, 1]

Le glouton prend la pièce de 4, se retrouve avec 2 à rendre, ne peut plus utiliser ni le 4 ni le 3, et termine avec deux pièces de 1 : trois pièces au total. Or 6 s'écrit 3 + 3, soit deux pièces. Le glouton n'est pas optimal sur ce jeu de pièces, et il suffisait de la somme 6 pour le montrer.

Que 2 soit le minimum reste à établir. La figure suivante l'établit par énumération : elle parcourt toutes les combinaisons de pièces de 4, de 3 et de 1, a, b et c comptant les pièces de chaque sorte, et retient la plus courte de celles dont le total fait 6. Chaque boucle s'arrête au plus grand nombre de pièces qui tient dans 6 : une pièce de 4, puisque deux font déjà 8, deux pièces de 3, puisque trois font 9, et six pièces de 1. La valeur 99 donnée au départ à meilleur signifie « aucune solution encore trouvée » : plus grande que tout nombre de pièces possible ici, elle est remplacée par la première combinaison qui convient.

Algorithme
pas 1 / 114
Début
cible ← 6
meilleur ← 99
Pour a de 0 à 1
Pour b de 0 à 2
Pour c de 0 à 6
Si 4 * a + 3 * b + c = cible Alors
Si a + b + c < meilleur Alors
meilleur ← a + b + c
Écrire a, " pièce(s) de 4, ", b, " de 3, ", c, " de 1 -> ", meilleur
FinSi
FinSi
FinPour
FinPour
FinPour
Écrire "optimum : ", meilleur, " pièces"
Fin

programme principal

cible6

La trace affiche successivement 6 pièces, puis 4, puis 2, et conclut sur optimum : 2 pièces. Le contre-exemple est établi, pas supposé.

Un glouton qui marche sur les données d'essai n'est pas un glouton correct
Sur ce même jeu de pièces 4, 3 et 1, le glouton donne le bon résultat pour toutes les sommes de 1 à 9, sauf 6. Il se trompe ensuite sur 10, 14, 18, et sur toute somme de la forme 4k + 2 à partir de 6, soit une somme sur quatre. Un jeu d'essai fait de 1, 2, 3, 4, 5, 7, 8, 9, 11 et 12 l'aurait pourtant déclaré correct.
C'est exactement la situation décrite dans le chapitre sur la preuve d'un algorithme : l'essai qui passe ne prouve rien, et il faut soit une démonstration, soit un contre-exemple.
La question à poser avant d'écrire un glouton

« Existe-t-il un cas où le meilleur choix immédiat interdit le meilleur choix global ? » Ici, prendre la pièce de 4 laisse un reste de 2 que le jeu de pièces ne sait pas payer efficacement. Un glouton ne se juge pas en le testant : il se juge en démontrant que le choix local ne détruit jamais l'optimum, ou en trouvant le contre-exemple qui montre qu'il le détruit.

Vérification rapideon peut se reprendre

Sur le jeu de pièces 4, 3 et 1, quelle est la plus petite somme qui met le glouton en défaut ?

Recalculer cent fois la même chose

Quand le glouton échoue, il faut envisager plusieurs choix à chaque étape au lieu d'un seul. Sur le rendu de monnaie, l'idée se dit simplement. Pour payer 6, la dernière pièce posée est une pièce de 1, de 3 ou de 4 : il suffit d'essayer les trois, de payer au mieux ce qui reste, 5, 3 ou 2, et de garder la meilleure des trois possibilités. Or payer au mieux ce qui reste est le même problème sur une somme plus petite : la fonction s'appelle elle-même, comme au chapitre sur la récursivité, et la descente s'arrête sur la somme 0, qui se paie sans aucune pièce. La figure affiche chaque appel au moment où il se produit.

Algorithme
pas 1 / 297
Fonction Rendre(s)
Écrire "appel de Rendre(", s, ")"
Si s = 0 Alors
Retourner 0
FinSi
pieces ← [1, 3, 4]
meilleur ← 99
Pour i de 1 à longueur(pieces)
Si pieces[i] <= s Alors
essai ← Rendre(s - pieces[i]) + 1
Si essai < meilleur Alors
meilleur ← essai
FinSi
FinSi
FinPour
Retourner meilleur
FinFonction
Début
Écrire "rendre 6 : ", Rendre(6), " pièce(s)"
Fin

programme principal

aucune variable

La réponse est juste, deux pièces, et cette fois sans pari. La valeur 99 donnée au départ à meilleur joue le même rôle que dans l'énumération : elle signifie « aucune solution encore trouvée ». Mais la sortie compte vingt-quatre appels pour une somme de 6, et la répétition saute aux yeux : Rendre(3) est calculé deux fois, Rendre(2) quatre fois, Rendre(1) six fois et Rendre(0) neuf fois. Aucune de ces réponses ne change d'un calcul à l'autre, et pourtant chacune est recalculée de zéro.

Le compteur de pas mesure la croissance. La figure suivante retire la ligne qui annonce chaque appel et lit la somme à rendre dans son champ de saisie : la somme 6 y demande 274 pas, la somme 7 en demande 443, et la somme 8, proposée à l'ouverture, ne se déroule pas jusqu'au bout.

Algorithme
pas 1 / 501
séparées par des virgules, décimales au point
Fonction Rendre(s)
Si s = 0 Alors
Retourner 0
FinSi
pieces ← [1, 3, 4]
meilleur ← 99
Pour i de 1 à longueur(pieces)
Si pieces[i] <= s Alors
essai ← Rendre(s - pieces[i]) + 1
Si essai < meilleur Alors
meilleur ← essai
FinSi
FinSi
FinPour
Retourner meilleur
FinFonction
Début
Lire somme
Écrire "rendre ", somme, " : ", Rendre(somme), " pièce(s)"
Fin

programme principal

sommesaisi8

Le moteur s'arrête au-delà de cinq cents pas et le signale. Le message qu'il affiche évoque une boucle dont rien ne fait avancer la condition de sortie, parce que c'est la cause habituelle ; ici la cause est autre, et plus inquiétante : l'algorithme se termine parfaitement, il demande simplement plus de pas que la page n'en déroule. Une somme de 8, que deux pièces de 4 suffisent à payer, épuise déjà le budget. Chaque unité de plus multiplie le travail par un facteur à peu près fixe, voisin de 1,6, parce que chaque appel en relance jusqu'à trois autres.

Le mur est là bien avant les grands nombres. Un coût qui se multiplie par un facteur fixe à chaque unité de plus devient inatteignable en quelques dizaines d'unités, quelle que soit la machine. La suite de Fibonacci, où chaque terme est la somme des deux précédents, en donne l'exemple le plus connu : écrite de la même façon récursive, elle fait quinze appels pour obtenir son sixième terme, 8, et calcule cinq fois le deuxième. La comparaison des classes de coût et la notation qui les décrit sont traitées dans le chapitre sur la complexité.

Retenir au lieu de recalculer

Le gaspillage a une cause unique : la même question est posée plusieurs fois, et la réponse n'est jamais conservée. Deux remèdes existent.

La mémoïsation garde la récursion telle quelle et lui adjoint un carnet partagé par tous les appels : avant de calculer, chaque appel regarde si la réponse y figure déjà, et l'y inscrit sinon. Ce carnet commun n'existe pas dans le pseudo-code du cours, où une fonction ne communique que par ses paramètres, reçus en copie, et par sa valeur de retour : ce qu'un appel noterait dans sa copie serait perdu à son retour. Python, qui partage au contraire une liste ou un dictionnaire passé en paramètre, permet de l'écrire en quelques lignes.

Le second remède obtient le même gain sans récursion. Il part des plus petites sommes et monte, en rangeant chaque réponse dans un tableau où les suivantes viennent la relire : quand la somme 6 a besoin de la meilleure façon de payer 3, celle-ci est déjà écrite, et il suffit de la lire. Cette façon de faire porte un nom, la programmation dynamique, et la section suivante la construit sur le rendu de monnaie.

Vérification rapideon peut se reprendre

Pourquoi la version récursive du rendu de monnaie coûte-t-elle si cher ?

Remplir le tableau de proche en proche

Le second remède n'est pas un pis-aller : c'est le principe même de la programmation dynamique.

Définition

La programmation dynamique résout un problème en remplissant un tableau des solutions de tous les sous-problèmes, du plus petit au plus grand, chaque case se déduisant de cases déjà remplies. Aucun sous-problème n'est traité deux fois, et l'ordre de remplissage est décidé par l'algorithme lui-même au lieu d'être dicté par l'ordre des appels récursifs.

L'idée est celle de la fonction Rendre, débarrassée de ses répétitions. Pour rendre la somme s au mieux, on essaie chaque pièce p comme dernière pièce, on lui ajoute la meilleure façon de rendre le reste s - p, et on garde la plus petite des possibilités. Le reste doit lui-même être payé au mieux : une solution qui paierait mal son reste s'améliorerait en remplaçant ce reste par une meilleure façon de le payer. Le tableau m range ces réponses, m[s] étant le nombre minimal de pièces pour la somme s, et chaque case se calcule à partir de cases plus petites, déjà remplies.

Deux détails du pseudo-code tiennent aux conventions du cours. La case m[0], qui vaudrait 0, n'existe pas, puisque les tableaux commencent à 1 : le cas où la pièce vaut exactement la somme, p = s, est donc traité à part, et il donne une seule pièce, le minimum possible. Et chaque case démarre à 99, qui signifie, comme dans Rendre, « aucune solution encore trouvée ». La figure affiche la proposition de chaque pièce avant de conclure sur la case, et son champ de saisie règle la somme à rendre.

Algorithme
pas 1 / 129
séparées par des virgules, décimales au point
Début
pieces ← [1, 3, 4]
Lire cible
m ← tableau[cible]
Pour s de 1 à cible
m[s] ← 99
Pour i de 1 à longueur(pieces)
p ← pieces[i]
Si p = s Alors
m[s] ← 1
Écrire "somme ", s, ", pièce de ", p, " : seule, 1 pièce"
SinonSi p < s Alors
Écrire "somme ", s, ", pièce de ", p, " : m[", s - p, "] + 1 = ", m[s - p] + 1
Si m[s - p] + 1 < m[s] Alors
m[s] ← m[s - p] + 1
FinSi
FinSi
FinPour
Écrire "donc m[", s, "] = ", m[s]
FinPour
Fin

programme principal

pieces[1, 3, 4]

Le tableau se remplit de gauche à droite, en 129 pas. Chaque ligne ci-dessous reprend un tour de la boucle extérieure : les trois colonnes du milieu sont les propositions des trois pièces, et la dernière retient la plus petite.

Somme spièce de 1pièce de 3pièce de 4m[s]
1seule : 1trop grandetrop grande1
2m[1] + 1 = 2trop grandetrop grande2
3m[2] + 1 = 3seule : 1trop grande1
4m[3] + 1 = 2m[1] + 1 = 2seule : 11
5m[4] + 1 = 2m[2] + 1 = 3m[1] + 1 = 22
6m[5] + 1 = 3m[3] + 1 = 2m[2] + 1 = 32

La case 6 vaut 2, la réponse de l'énumération exhaustive, obtenue cette fois en ne calculant chaque somme qu'une fois. Sa ligne résume le chapitre : la pièce de 4, celle que le glouton prend d'emblée, propose 3 ; la pièce de 3 propose 2, et c'est elle qui l'emporte. L'ordre des pièces dans le tableau pieces n'a d'ailleurs plus d'importance, puisque toutes sont essayées à chaque case.

Le champ de saisie permet de reprendre la comparaison avec la version récursive. Pour la somme 7, la plus grande que celle-ci déroule jusqu'au bout, elle demandait 443 pas ; la table en demande 153, en affichant pourtant chacune de ses propositions. Pour la somme 12, que la version récursive ne déroule pas, la table conclut à 3 pièces en 272 pas. L'écart n'est pas un gain de constante : la version récursive coûte un facteur à peu près fixe de plus par unité de somme, la table coûte un tour de boucle de plus par unité. La somme 10 montre au passage un second échec du glouton : la table y trouve 3 pièces, 4 + 3 + 3, là où le glouton en rend 4.

Ce que le tableau change par rapport au glouton. Le glouton prend une décision et l'oublie. Le tableau garde, pour chaque somme intermédiaire, la meilleure façon connue de la payer, et compare toutes les dernières pièces possibles avant de conclure. Il ne parie pas : il choisit après avoir vu.
Le prix de ce choix informé est le tableau lui-même, une case par sous-problème, là où le glouton ne retenait rien.

Il reste à savoir quelles pièces composent la solution, et pas seulement combien. Il suffit de noter, en même temps que le minimum, la pièce qui l'a produit, puis de remonter. La figure suivante reprend la même boucle, sans ses affichages, avec un tableau dernier en plus.

Algorithme
pas 1 / 129
séparées par des virgules, décimales au point
Début
pieces ← [1, 3, 4]
Lire cible
m ← tableau[cible]
dernier ← tableau[cible]
Pour s de 1 à cible
m[s] ← 99
Pour i de 1 à longueur(pieces)
p ← pieces[i]
Si p = s Alors
m[s] ← 1
dernier[s] ← p
SinonSi p < s Alors
Si m[s - p] + 1 < m[s] Alors
m[s] ← m[s - p] + 1
dernier[s] ← p
FinSi
FinSi
FinPour
FinPour
Écrire "minimum pour ", cible, " : ", m[cible], " pièces"
s ← cible
TantQue s > 0
Écrire "pièce de ", dernier[s]
s ← s - dernier[s]
FinTantQue
Fin

programme principal

pieces[1, 3, 4]
À manipuler
Saisir 14 dans le champ « Ce que Lire reçoit », dérouler jusqu'au bout et relever les pièces que nomme la sortie, puis recommencer avec 18. La table annonce 4 puis 5 pièces, et chaque fois deux pièces de 3 ouvrent la liste, suivies de pièces de 4. Le glouton, qui épuise les pièces de 4 avant de compléter, en rendrait 5 puis 6 : il finit par une pièce de 4 et deux pièces de 1, là où la table met deux pièces de 3. Noter aussi le compteur de pas en fin de déroulement, 307 pour 14 et 396 pour 18 : 89 pas de plus pour 4 unités de somme en plus, une croissance régulière, sans rien de l'explosion de la version récursive. Saisir enfin 2 : ni 3 ni 4 n'y tiennent, et la table rend deux pièces de 1, comme le glouton.

La sortie annonce deux pièces, puis les nomme : une pièce de 3, puis une autre. Le tableau dernier vaut [1, 1, 3, 4, 1, 3] : la pièce notée pour 6 est un 3, la remontée passe à la somme 3, dont la pièce notée est encore un 3, et arrive à 0. Avec 10, elle nomme 3, 3 puis 4, la solution que le glouton manquait. La boucle de remontée a son variant, s, qui décroît strictement puisque dernier[s] vaut au moins 1, et qui ne descend jamais sous 0, puisque la pièce notée pour une somme ne la dépasse jamais : elle s'arrête donc toujours.

Vérification rapideon peut se reprendre

1.Pour la somme 7, la version récursive demande 443 pas et la table 153. Que mesure cet écart ?

2.Par rapport au glouton, que fait de plus chaque case de la table du rendu de monnaie ?

Mettre en pratique

Le glouton se compte pièce par pièce, et la table case par case, chaque case ne lisant que des cases déjà remplies. Les deux calculs se font à la main.

Le glouton, la table et l'explosion, en nombres

  • 1.

    Sur le jeu de pièces 1, 5 et 12, combien de pièces le glouton rend-il pour la somme 20 ?

  • 2.

    Sur le même jeu, combien de pièces suffisent au minimum pour rendre ces 20 ?

  • 3.

    Sur le jeu 4, 3 et 1, la table du chapitre continue au-delà de 6. Que vaut m[9] ?

  • 4.

    Combien d'appels la version récursive fait-elle pour rendre 7, en comptant l'appel de départ ?

  • 5.

    Pour rendre 100, la table remplit les cases 1 à 100 avec les trois pièces 1, 3 et 4. Combien de propositions examine-t-elle en tout, une proposition étant une pièce qui ne dépasse pas la somme de la case ?

Les deux dernières réponses mesurent l'écart que le compteur de pas montrait : la récursion multiplie ses appels à chaque unité de somme en plus, la table n'ajoute que trois propositions par case.

Trouver une somme que le glouton paie mal sur le jeu de pièces 1, 5 et 12.

La somme 15. Le glouton prend 12, puis trois pièces de 1 : quatre pièces, contre trois pour la solution 5 + 5 + 5. Les deux nombres ont été relevés au moteur, le premier sur le glouton et le second sur le tableau du rendu de monnaie.

La recherche peut être rendue systématique grâce à un résultat démontré par Kozen et Zaks en 1994 : si le glouton se trompe un jour sur un jeu de pièces, il se trompe déjà sur une somme inférieure à la somme des deux plus grosses pièces. Il suffit donc d'essayer toutes les sommes jusqu'à 16, puisque 12 + 5 = 17, en comparant le glouton au tableau : 15 est la première qui les sépare, et 16 la suit. Sur le jeu 4, 3, 1, la borne vaut 4 + 3 = 7, et l'essai des sommes 1 à 6 désigne 6.

Donner l'invariant de la boucle qui remplit le tableau du rendu de monnaie.

L'invariant s'énonce après chaque tour, comme pour tout Pour : après le tour numéro s, les cases m[1] à m[s] contiennent le nombre minimal de pièces nécessaires pour rendre respectivement les sommes 1 à s.

Avant le premier tour, aucune case n'est remplie et il n'y a rien à vérifier. La conservation demande deux arguments. Le premier est l'ordre de remplissage : le tour s ne lit que des cases m[s - p], d'indice strictement inférieur à s, donc déjà remplies et justes. Le second est la formule elle-même. Chaque proposition m[s - p] + 1 est réalisable : il suffit d'ajouter la pièce p à une façon optimale de rendre s - p. Inversement, une façon optimale de rendre s se termine par une certaine pièce p, et les pièces qui la précèdent rendent s - p ; elles sont au moins au nombre de m[s - p], sinon m[s - p] ne serait pas le minimum. Le meilleur total est donc exactement la plus petite des propositions, ou 1 si une pièce vaut s, et c'est ce que le tour calcule. Après le dernier tour, s vaut cible, comme l'affiche la figure, et m[cible] est le minimum cherché.

C'est le point exact où la programmation dynamique se prouve : l'ordre de remplissage garantit qu'une case n'est lue qu'après avoir été écrite, et la décomposition par la dernière pièce garantit que la case écrite est juste.

Dire lequel des deux algorithmes, glouton ou tableau, convient pour rendre la monnaie dans une caisse de supermarché français.

Le glouton, sans hésitation. Il est optimal sur le système de pièces européen, il ne demande aucun tableau, et son coût est celui d'un simple parcours du jeu de pièces. Écrire la version dynamique là où le glouton est démontré optimal ajouterait de la mémoire et du calcul pour un résultat identique.

Le choix ne porte donc pas sur la méthode la plus puissante mais sur la plus simple qui soit correcte, et savoir laquelle est correcte demande une preuve, pas un essai.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices