Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de:
- reconnaître un algorithme glouton à sa structure — une suite de décisions locales, irrévocables, prises selon un critère fixé — et nommer les deux propriétés qu'il exige de son problème: la propriété du choix glouton et la sous-structure optimale;
- écrire un argument d'échange complet, c'est-à-dire transformer une solution optimale quelconque en la solution gloutonne sans jamais dégrader sa valeur, et reconnaître les quatre étapes de cet argument dans une démonstration que vous lisez;
- démontrer l'optimalité de l'ordonnancement d'intervalles par date de fin la plus tôt, et réfuter par un contre-exemple explicite les deux critères plausibles que sont la durée la plus courte et la date de début la plus tôt;
- construire un arbre de Huffman à la main sur une instance donnée, en déduire les codes, compter les bits obtenus, et démontrer l'optimalité de l'arbre par échange;
- distinguer les problèmes où le glouton est optimal — sac à dos fractionnaire, rendu de monnaie sur le système suisse — de ceux où il ne l'est pas, sac à dos 0/1 et systèmes de pièces non canoniques, et dire ce qui change exactement;
- expliquer pourquoi «mon algorithme donne la bonne réponse sur mes trois exemples» n'est pas une démonstration, et ce qu'une recherche exhaustive de contre-exemples prouve — et ne prouve pas.
Ce qu'est un algorithme glouton
Décider une fois, ne jamais revenir
Les chapitres précédents résolvaient des problèmes où la structure de la solution était connue d'avance: trier un tableau, c'est produire une permutation; parcourir un graphe, c'est visiter tous les sommets. Les problèmes de ce chapitre et du suivant sont d'une autre nature. On vous donne un ensemble de possibilités, une contrainte, une mesure de valeur, et on vous demande de choisir un sous-ensemble: les réunions à placer dans une salle, les objets à emporter, les bits à donner à chaque caractère. Le nombre de sous-ensembles est exponentiel, l'énumération est hors de portée dès la cinquantaine d'éléments — le tableau du chapitre 1 le disait déjà: dépasse le milliard d'opérations à —, et il faut donc une méthode qui ne les regarde pas tous.
La méthode la plus simple qu'on puisse imaginer consiste à décider élément par élément, sans jamais revenir en arrière.
Deux mots de cette définition font tout le travail, et ce sont ceux dont dépendent à la fois l'efficacité et la difficulté du sujet.
Local. Le critère ne regarde pas l'avenir. Pour choisir la prochaine réunion, il ne simule pas ce que ce choix interdira; il applique une règle — «celle qui finit le plus tôt», «la plus courte», «celle qui commence en premier» — à ce qui reste disponible. C'est ce qui rend l'algorithme rapide: le schéma usuel est un tri, en comparaisons, suivi d'un unique parcours linéaire. Comparez avec le chapitre 10, où la programmation dynamique remplit une table de cases précisément parce qu'elle refuse de décider sans avoir comparé les deux branches.
Irrévocable. Une fois la réunion placée, elle y reste. C'est ce qui rend l'algorithme dangereux: si le critère se trompe une fois, rien dans la suite de l'exécution ne le rattrapera. Un glouton n'a pas de filet.
Le prix de cette simplicité est qu'un algorithme glouton n'est presque jamais optimal. Sur la plupart des problèmes de choix, aucun critère local ne peut l'être, et le chapitre 10 existe pour cette raison. Les quelques problèmes où un glouton est optimal sont donc précieux — et chacun d'eux mérite une démonstration, pas une conviction.
Les deux propriétés dont il a besoin
Que faut-il exactement au problème pour qu'un glouton l'emporte? Deux propriétés, et elles ne disent pas la même chose.
La sous-structure optimale est ce qui autorise la récurrence: elle dit que résoudre l'instance réduite optimalement suffit. Elle est partagée avec la programmation dynamique, qui l'exige tout autant — la différence entre les deux méthodes ne tient pas là. La propriété du choix glouton, elle, est spécifique: elle dit qu'on peut choisir sans comparer. C'est elle qui ramène un arbre de décision à un chemin.
La propriété du choix glouton affirme que:
L'argument d'échange
Une technique de démonstration mérite d'être enseignée pour elle-même quand elle sert cinq fois dans un chapitre. Celle-ci, appelée argument d'échange (exchange argument), est la démonstration standard d'optimalité d'un algorithme glouton, et vous l'avez déjà rencontrée au chapitre 8 sous une forme déguisée — la propriété de coupe qui justifie Kruskal est un argument d'échange sur les arêtes d'un arbre couvrant. Ce chapitre-ci ne fait que la nommer et la systématiser.
L'idée
On ne cherche pas à montrer que le glouton trouve *l'*optimum, ce qui n'aurait pas de sens puisqu'il y en a souvent plusieurs. On montre que toute solution optimale peut être transformée, pas à pas, en la solution gloutonne, chaque pas conservant la valeur. Si une telle transformation existe, la solution gloutonne est aussi bonne que celle dont on est parti; comme on est parti d'un optimum, elle est optimale.
L'image est celle d'un déménagement: on prend l'optimum de quelqu'un d'autre et on y substitue, un objet à la fois, les objets du glouton, en vérifiant à chaque substitution que rien n'est cassé ni perdu.
Deux variantes du même argument
La forme ci-dessus échange un élément. Une variante fréquente échange des positions dans l'arbre (c'est celle de Huffman: on échange deux feuilles), et une autre, dite greedy stays ahead, ne construit pas du tout: elle démontre par récurrence que le glouton est, après chaque étape, au moins aussi avancé que n'importe quelle solution — la -ième tâche du glouton finit au plus tôt que la -ième tâche de toute autre solution. Les deux formes sont interchangeables sur l'ordonnancement d'intervalles; nous écrirons l'échange, parce que c'est celle qui se transpose à Huffman et au sac à dos fractionnaire.
Remettez dans l'ordre les étapes d'un argument d'échange.
Glissez les éléments pour les mettre dans le bon ordre
- Prendre une solution optimale quelconque , sans rien supposer d'autre
- Vérifier que est encore admissible, puis que sa valeur n'est pas dégradée
- Remplacer ce choix par celui du glouton, obtenant
- Conclure que s'obtient en un nombre fini d'échanges, donc qu'elle est optimale
- Repérer le premier choix sur lequel diffère de la solution gloutonne
Ordonnancement d'intervalles
Le problème
Une salle de séminaire, une seule, et une pile de demandes de réservation. Chaque demande est un intervalle: elle commence à une date, se termine à une autre, et n'est pas négociable. Deux demandes qui se chevauchent ne peuvent pas être satisfaites toutes les deux. On veut en satisfaire le plus grand nombre possible — remarquez bien: le plus grand nombre, pas la plus grande durée occupée, ce qui serait un autre problème et n'aurait pas la même réponse.
Le choix de l'intervalle semi-ouvert n'est pas cosmétique: il dit qu'une tâche qui finit à 13h et une tâche qui commence à 13h sont compatibles, ce qui est la convention naturelle pour une salle et la convention des indices de ce cours — le chapitre 1 a fixé que les tranches sont semi-ouvertes, et les intervalles de temps suivent la même règle.
Voici l'instance sur laquelle tout ce chapitre travaillera. Ces huit tâches sont fictives; elles n'ont d'autre vertu que d'être toujours les mêmes, et les dates sont des entiers pour que vous puissiez refaire tous les calculs de tête.
| Tâche | T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 |
|---|---|---|---|---|---|---|---|---|
| début | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 |
| fin | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 |
| durée | 3 | 2 | 6 | 2 | 6 | 4 | 4 | 3 |
Le critère qui marche
L'algorithme tient en dix lignes. Écrivons-le avant de le démontrer, parce qu'il faut savoir exactement ce qu'on démontre.
def ordonnancement(taches):
"""Taches = liste de (nom, debut, fin). Renvoie un ordonnancement maximal."""
retenues = []
fin_courante = float("-inf")
for tache in sorted(taches, key=lambda t: t[2]): # tri par date de fin
if tache[1] >= fin_courante: # compatible avec le reste
retenues.append(tache)
fin_courante = tache[2]
return retenues
Le coût se lit sur le code: un tri, donc comparaisons dans le pire des cas avec un tri par comparaison du chapitre 3, puis un parcours qui effectue exactement comparaisons de dates, une par tâche. Le tri domine, et le total est comparaisons dans le pire des cas. Si les tâches arrivent déjà triées par date de fin — ce qui est le cas d'un journal d'événements, par exemple —, il ne reste que le parcours linéaire.
Notez la variable fin_courante: c'est tout l'état de l'algorithme. Il n'a pas besoin de connaître les tâches déjà retenues, seulement la date à laquelle la salle se libère. C'est la signature d'un glouton bien posé.
Démonstration du théorème 9.1. Notons les tâches retenues par l'algorithme, dans l'ordre où il les retient, et donc par dates de fin croissantes. Soit une solution optimale quelconque, ses tâches étant également rangées par dates de fin croissantes. Comme est optimale, ; nous voulons .
L'échange. Montrons d'abord la propriété suivante, pour tout compris entre et :
il existe une solution optimale dont les premières tâches sont exactement .
Raisonnons par récurrence sur .
Pour : soit une solution optimale. Sa première tâche a une date de fin . Or est, par construction, la tâche de date de fin minimale de toute l'instance, donc . Posons . Cette est : les tâches commencent toutes après la fin de , donc après , donc après la fin de ; elles restent compatibles entre elles, et le sont maintenant avec . Et : on a retiré une tâche et ajouté une tâche, donc est encore optimale. C'est l'échange, et il a bien fallu vérifier les deux moitiés.
Pour le pas de récurrence, supposons qu'il existe une solution optimale dont les premières tâches sont , avec . Si n'a pas de -ième tâche, alors , ce qui contredit l'optimalité de : donc possède une tâche . Cette tâche est compatible avec , c'est-à-dire qu'elle commence après ; elle était donc au moment où l'algorithme a choisi , c'est-à-dire qu'elle faisait partie des tâches que la boucle examinait encore. Comme l'algorithme parcourt les tâches par date de fin croissante et retient la première compatible, on a . Le même échange qu'au cas de base — remplacer par — donne une solution admissible (les tâches commencent après ) et de même cardinal, donc optimale, et qui coïncide avec sur tâches.
La conclusion. Appliquons la propriété avec : il existe une solution optimale dont les premières tâches sont . Supposons que en contienne une de plus, disons . Alors commence après ; elle était donc compatible avec toutes les tâches retenues au moment où la boucle l'a examinée, et l'algorithme l'aurait retenue. Or il s'est arrêté avec tâches: contradiction. Donc , et comme est optimale, l'est aussi.
Relisez l'endroit exact où le critère est utilisé: une seule fois, à l'inégalité , et cette inégalité est l'énoncé littéral du critère. C'est toujours ainsi qu'un argument d'échange se lit: il y a une ligne et une seule où l'algorithme entre, et c'est celle-là qu'il faut regarder quand on teste un autre critère.
Deux critères plausibles, et pourquoi ils échouent
Trois critères viennent naturellement à l'esprit devant ce problème: la tâche qui finit le plus tôt, la tâche la plus courte, la tâche qui commence le plus tôt. Le premier est optimal, les deux autres ne le sont pas, et aucun raisonnement vague ne permet de les départager. Il faut des contre-exemples, et les voici — construits, puis vérifiés en exécutant les deux algorithmes.
La durée la plus courte. L'intuition est séduisante: une tâche courte occupe peu la salle, donc en laisse plus pour les autres. Elle est fausse, parce qu'une tâche courte peut être placée exactement à cheval sur deux tâches longues et les tuer toutes les deux. Trois réunions suffisent:
| Réunion | A | B | C |
|---|---|---|---|
| horaire | 9h – 13h | 12h – 14h | 13h – 17h |
| durée | 4 h | 2 h | 4 h |
Le critère de la durée retient B, qui dure deux heures, et se retrouve bloqué: A commence à 9h et finit à 13h, donc chevauche B; C commence à 13h, ce qui est avant la fin de B à 14h, donc chevauche B aussi. Une réunion placée. Le critère de la date de fin retient A (fin 13h), rejette B (début 12h, salle libre à 13h), retient C (début 13h): deux réunions. L'énumération des huit sous-ensembles confirme que deux est l'optimum. Le rapport est donc de un à deux, sur une instance de trois éléments.
La date de début la plus tôt. L'intuition est ici celle du guichet: servir les demandes dans l'ordre où elles arrivent. Elle est fausse pour une raison symétrique — la première demande arrivée peut être une réunion d'une journée entière, qui écrase tout ce qui suit.
| Réunion | A | B | C | D |
|---|---|---|---|---|
| horaire | 8h – 18h | 9h – 11h | 11h – 14h | 14h – 17h |
Le critère du début le plus tôt retient A et s'arrête: une réunion. Le critère de la date de fin retient B, C, D: trois réunions, et c'est l'optimum. Le rapport est de un à trois, et il s'aggrave autant qu'on veut en ajoutant des réunions courtes dans la journée de A.
Ce second critère est d'ailleurs déjà pris en défaut par l'instance témoin des huit tâches: il retient T3 , puis T7 , soit deux tâches contre les trois du bon critère. Vous n'aviez pas besoin d'une nouvelle instance pour le réfuter — mais vous aviez besoin de faire le calcul.
Sur l'instance des trois réunions A 9h–13h, B 12h–14h, C 13h–17h, combien de réunions le critère de la durée la plus courte place-t-il de moins que l'optimum?
Écrivez le glouton par date de fin. La fonction doit renvoyer la liste des tâches retenues, dans l'ordre chronologique; le programme affiche leur nombre puis leurs noms. Attention: le contrôle appellera aussi votre fonction sur une instance que vous n'avez jamais vue.
L'explorateur ci-dessous vous laisse déplacer trois des six réunions. Le seul affichage à surveiller est le troisième: c'est celui qui ne bouge pas.
Six réunions se disputent une seule salle, de 8h à 20h. Les trois curseurs déplacent le début de B, la durée de D et le début de F; A, C et E ne bougent pas. Le panneau du haut montre ce que retient le glouton par date de fin la plus tôt, celui du bas un ordonnancement optimal trouvé par énumération des 64 sous-ensembles. Cherchez une position des curseurs qui mette le glouton en défaut: l’écart affiché reste à zéro, alors que les deux critères naïfs affichés en dessous décrochent souvent.
Codage de Huffman
Pourquoi des mots de longueurs différentes
Un texte de cent caractères écrits sur un alphabet de six symboles peut se coder sur bits par caractère, soit 300 bits. Ce codage à longueur fixe a une qualité: le décodage est trivial, on découpe la suite de bits en tranches de trois. Il a un défaut: il donne autant de bits au caractère le plus fréquent qu'au plus rare. Si un caractère apparaît dans 45 % du texte, lui accorder trois bits est un gaspillage de plusieurs dizaines de bits.
L'idée du codage à longueur variable est de donner des mots courts aux caractères fréquents et des mots longs aux caractères rares. Elle soulève immédiatement une difficulté: si tous les mots n'ont pas la même longueur, comment le décodeur sait-il où s'arrête le premier? La réponse tient dans une contrainte structurelle.
L'ambiguïté qu'évite cette contrainte n'est pas théorique. Prenons le code , , : il n'est pas préfixe, puisque est le préfixe de . La suite de bits 01 se lit alors soit comme , soit comme suivi de . Aucune quantité d'ingéniosité du décodeur ne lèvera cette ambiguïté: l'information n'est pas là.
Démonstration. Étant donné un arbre binaire dont les feuilles portent les caractères, le mot d'un caractère est le chemin qui mène à sa feuille. Un mot est préfixe d'un autre si et seulement si le chemin correspondant est un début du second, c'est-à-dire si la première feuille est un ancêtre de la seconde — impossible, puisqu'une feuille n'a pas de descendant. Le code est donc préfixe.
Réciproquement, donnons-nous un code préfixe et construisons l'arbre en insérant chaque mot comme un chemin depuis la racine. Aucun mot n'étant préfixe d'un autre, aucun caractère ne se retrouve sur un nœud interne du chemin d'un autre; chaque caractère occupe donc bien une feuille.
La relation (9.1) est une simple comptabilité: le caractère apparaît fois dans le texte et coûte bits à chaque apparition.
Reste la complétude locale. Supposons qu'un nœud interne n'ait qu'un seul enfant. Remplaçons par cet unique enfant: toutes les feuilles du sous-arbre remontent d'un niveau, donc leur profondeur diminue de 1, et diminue strictement dès qu'il y a au moins une feuille dessous — ce qui est le cas puisque l'arbre est fini et que tout chemin descendant aboutit à une feuille. Un arbre qui n'est pas localement complet n'est donc pas optimal.
L'algorithme de Huffman
Le critère glouton de Huffman est d'une simplicité désarmante: fusionner à chaque étape les deux nœuds de plus petit poids. On part d'une forêt de feuilles, une par caractère, portant sa fréquence; on répète fois: retirer les deux arbres les plus légers, en faire les deux enfants d'un nouveau nœud dont le poids est la somme, remettre ce nœud dans la forêt. À la fin il reste un seul arbre.
import heapq
def huffman(frequences):
"""frequences: dict caractere -> effectif. Renvoie la racine de l'arbre."""
tas = [(f, i, (c, None, None)) for i, (c, f) in enumerate(sorted(frequences.items()))]
heapq.heapify(tas)
rang = len(tas)
while len(tas) > 1:
f1, _, gauche = heapq.heappop(tas)
f2, _, droite = heapq.heappop(tas)
heapq.heappush(tas, (f1 +
Le second champ du triplet, rang, ne sert qu'à départager deux nœuds de même poids de façon déterministe, pour que la trace du cours soit reproductible: à poids égal, le nœud créé le plus tôt sort le premier. Sans ce champ, Python tenterait de comparer les nœuds eux-mêmes et lèverait une exception; avec un autre départage, on obtiendrait un arbre différent, de même coût total — l'optimum n'est pas unique, ce qui est exactement la situation que la propriété du choix glouton décrit.
Le coût: une construction de tas en par la méthode de Floyd du chapitre 5, puis tours de boucle comportant chacun deux extractions et une insertion, donc comparaisons chacun. Total: comparaisons, où est la taille de l'alphabet et non la longueur du texte.
L'arbre lu donne les codes, et les codes donnent le compte. Voici les deux ensemble.
| Caractère | total | ||||||
|---|---|---|---|---|---|---|---|
| fréquence | 45 | 12 | 13 | 16 | 5 | 9 | 100 |
| code | 0 | 100 | 101 | 111 | 1100 | 1101 | |
| longueur | 1 | 3 | 3 | 3 | 4 | 4 | |
| bits | 45 | 36 | 39 | 48 | 20 | 36 | 224 |
contre bits pour le codage à longueur fixe, soit 2,24 bits par caractère au lieu de 3 et une économie de 25,3 %. À titre de repère, l'entropie de cette distribution — la borne inférieure de la théorie de l'information, hors programme de ce cours — vaut environ 2,22 bits par caractère: Huffman est à deux centièmes de bit du minimum absolu.
Combien de bits faut-il pour coder le mot «serein» avec les codes de Huffman de l'instance du cours, c'est-à-dire , , , , , ?
Pourquoi c'est optimal
La démonstration se fait en deux temps, et le premier temps est un argument d'échange pur. Il répond à la question: pourquoi a-t-on le droit de fusionner les deux plus petits poids?
Démonstration. Soit un arbre optimal quelconque. Comme il est localement complet (théorème 9.2), une feuille de profondeur maximale y possède une sœur, qui est également une feuille de profondeur maximale. Appelons et ces deux sœurs.
Sans perte de généralité, supposons et . On a alors et , puisque et sont de fréquences minimales.
Premier échange. Construisons en échangeant les positions de et de dans . Le coût varie de
Le premier facteur est puisque ; le second est puisque est à profondeur maximale. Le produit est donc : on a . Comme était optimal, et est optimal aussi.
Second échange. Dans , échangeons de même et ; le même calcul donne un arbre avec , donc optimal. Une précaution s'impose ici: le premier échange a pu déplacer , mais il n'a pas pu l'amener à la place de , car et une position ne reçoit qu'un caractère; l'argument reste valable en appliquant le second échange à la position occupée par dans .
Dans , et occupent les positions de et , qui étaient sœurs et à profondeur maximale. L'arbre est optimal, et il répond à la question.
Ce lemme est la propriété du choix glouton pour Huffman, énoncée et démontrée. Reste la sous-structure optimale.
Démonstration. Par récurrence sur le nombre de caractères.
Base. Pour , il n'existe qu'un seul arbre de code localement complet: une racine et deux feuilles, chaque caractère sur un bit. L'algorithme le produit, et il est trivialement optimal.
Hérédité. Supposons le résultat acquis pour caractères, avec . Soient et les deux caractères de fréquences minimales, ceux que l'algorithme fusionne en premier. Formons l'alphabet réduit en remplaçant et par un unique caractère de fréquence ; il compte caractères.
Le lien entre les deux coûts. À tout arbre sur correspond un arbre sur , obtenu en remplaçant la feuille par un nœud interne portant les deux feuilles et . Les profondeurs de tous les autres caractères sont inchangées, et et sont un niveau plus bas que ne l'était , d'où
La correspondance est réciproque: à tout arbre sur dans lequel et sont frères correspond un arbre sur vérifiant la même relation. Le terme étant une , minimiser sur les arbres où et sont frères revient exactement à minimiser sur . C'est la sous-structure optimale.
La conclusion. Notons l'arbre produit par l'algorithme sur . Après la première fusion, l'algorithme travaille exactement sur — la forêt qu'il manipule est celle de l'instance réduite —, donc le sous-arbre qu'il construit est l'arbre de Huffman de , optimal sur par hypothèse de récurrence. Par (9.4), .
Soit maintenant un arbre optimal sur . Le lemme 9.3 permet de le choisir tel que et y soient frères; soit l'arbre réduit correspondant. Alors
la majoration venant de l'optimalité de sur . Donc , et est optimal.
La structure de cette démonstration mérite d'être retenue, car c'est celle de toutes les démonstrations d'optimalité d'un glouton: un lemme d'échange qui autorise le premier choix, une réduction qui montre que le reste est une instance du même problème, une récurrence qui recolle les deux. Le lemme d'échange est la partie difficile; la réduction est la partie qu'on oublie d'écrire.
Le coût d'un codage de Huffman sans construire l'arbre. En vertu de la relation (9.3), le nombre total de bits est la somme des poids de tous les nœuds internes, c'est-à-dire la somme des sommes calculées par les fusions. Complétez la fonction pour qu'elle renvoie ce total, et affichez-le suivi du nombre moyen de bits par caractère arrondi à deux décimales.
Dans un arbre de Huffman construit sur six caractères de fréquences deux à deux distinctes, que peut-on affirmer à coup sûr?
Le sac à dos fractionnaire
Le problème, et sa version entière
Un cambrioleur, un sac de capacité , et objets dont le -ième pèse et vaut . Il veut emporter le plus de valeur possible sans dépasser la capacité. Deux problèmes se cachent derrière cet énoncé, et ils n'ont pas la même difficulté.
Le second se résout gloutonnement et en comparaisons; le premier est -difficile (chapitre 11) et se résout par programmation dynamique au chapitre 10. La seule différence entre les deux énoncés est la clause «entier ou pas du tout», et cette clause change tout. Voir précisément où elle intervient est le but de cette section.
L'instance du cours est la suivante — c'est la même qu'au chapitre 10, pour que les deux traitements se comparent directement. Capacité ; quatre objets:
| Objet | ||||
|---|---|---|---|---|
| poids | 2 | 3 | 4 | 5 |
| valeur | 3 | 4 | 5 | 8 |
| rapport |
Le critère: la valeur par unité de poids
Le critère glouton naturel n'est ni la valeur, ni le poids, mais leur rapport: on remplit le sac en commençant par l'objet dont chaque kilo rapporte le plus, et on continue jusqu'à ce que le sac soit plein — le dernier objet étant pris fractionnairement, pour occuper exactement la place restante.
from fractions import Fraction
def sac_fractionnaire(objets, capacite):
"""objets = liste de (nom, poids, valeur). Renvoie (valeur totale, prises)."""
reste = Fraction(capacite)
valeur = Fraction(0)
prises = []
for nom, p, v in sorted(objets, key=lambda o: -Fraction(o[2], o[1])):
if reste == 0:
break
quantite
Les Fraction ne sont pas une coquetterie: le résultat de cette instance est , un rationnel non décimal, et l'écrire en virgule flottante donnerait avec une erreur au dernier chiffre. Le chapitre 1 d'Analyse numérique explique pourquoi; ici il suffit de savoir que, quand la réponse exacte est une fraction, on la calcule en fractions.
Démonstration. Supposons les objets numérotés par rapports décroissants: . Notons le vecteur des fractions produites par le glouton et celui d'une solution optimale quelconque.
Remarquons d'abord que le glouton remplit le sac: soit il épuise tous les objets, et alors aucune solution ne peut faire mieux puisqu'il a tout pris; soit il s'arrête faute de place, et alors . Plaçons-nous dans ce second cas.
Soit le plus petit indice où diffère de . Par construction du glouton, vaut 1 pour et le sac n'est pas encore plein après ces objets, donc est la plus grande fraction de l'objet que la place autorise. Comme coïncide avec avant et est admissible, on a nécessairement : la solution optimale prend de l'objet que le glouton.
L'échange. Posons , le poids manquant. Deux cas.
Si , il reste de la place dans et l'on peut y ajouter une partie de l'objet sans rien retirer; cela augmente strictement la valeur, ce qui contredit l'optimalité de . Donc remplit aussi le sac, et le poids manquant sur l'objet se retrouve nécessairement réparti sur des objets d'indice .
Construisons en retirant un poids total à ces objets d'indice — c'est possible puisqu'ils en portent au moins — et en l'ajoutant à l'objet , ce qui est licite puisque . La solution est : son poids total est inchangé, donc encore , et chaque fraction reste dans . Sa valeur varie de
où est le poids retiré à l'objet et où ; l'inégalité vient de pour . Donc est encore optimale, et elle coïncide avec sur un indice de plus.
La conclusion. Chaque échange augmente strictement le nombre d'indices initiaux où la solution coïncide avec ; comme il n'y a que indices, on atteint après au plus échanges, sans jamais perdre de valeur. Donc est optimale.
Ce que la clause «entier ou rien» détruit
Regardez l'endroit précis où la démonstration utilise la divisibilité: à l'échange, lorsqu'on retire «un poids total » à des objets d'indice supérieur. Dans la version 0/1, n'est pas un poids quelconque — on ne peut retirer que des objets entiers, et il n'y a en général aucune façon de retirer exactement . L'argument s'effondre là, et il s'effondre parce que le résultat est faux.
Trois nombres pour une instance
Cette instance mérite d'être regardée une dernière fois, parce qu'elle porte trois valeurs distinctes qu'il ne faut jamais confondre:
| Question posée | Réponse | Comment on l'obtient |
|---|---|---|
| Que trouve le glouton par sur le sac 0/1? | 11 | , poids 7, deux unités perdues |
| Quel est l'optimum du sac 0/1? | 13 | , poids exactement 9, seul ensemble à 13 |
| Quel est l'optimum du sac fractionnaire? | 13,67 | , , puis de , soit |
Ces trois nombres répondent à trois questions différentes sur les mêmes quatre objets. Le premier est ce que fait un algorithme; le deuxième est ce qu'il aurait fallu faire; le troisième est ce qu'on obtiendrait si la contrainte d'intégrité disparaissait. Et l'ordre n'est pas un accident: l'optimum fractionnaire majore toujours l'optimum 0/1, puisque toute solution 0/1 est une solution fractionnaire particulière. C'est précisément à ce titre que le chapitre 11 réutilisera la valeur fractionnaire — comme borne supérieure dans un algorithme d'approximation.
Et les autres critères gloutons?
Puisque le rapport échoue, essayons les deux autres critères naturels sur le sac 0/1. Le résultat est instructif, et il est un piège.
- Par valeur décroissante: on prend (valeur 8), puis (valeur 5) tient dans les 4 unités restantes, et l'on obtient , valeur 13. C'est l'optimum.
- Par poids croissant: on prend , , (poids ), valeur . Un de moins que l'optimum.
Remplissez le sac fractionnaire. La liste des objets est donnée sous la forme (nom, poids, valeur) et la capacité vaut 9. Complétez la fonction pour qu'elle renvoie la valeur totale exacte en Fraction, et affichez-la arrondie à deux décimales. Le contrôle appellera aussi votre fonction sur une autre instance.
Le rendu de monnaie
Le problème le plus familier du chapitre
Une caisse doit rendre un montant avec des pièces de valeurs données, en minimisant le nombre de pièces. L'algorithme que tout le monde applique sans y penser est glouton: prendre à chaque fois la plus grosse pièce qui ne dépasse pas ce qui reste à rendre.
def rendu_glouton(pieces, montant):
"""Rend le montant avec le moins de pieces possible... si le systeme s'y prete."""
rendu = []
for piece in sorted(pieces, reverse=True):
while montant >= piece:
rendu.append(piece)
montant -= piece
return rendu if montant == 0 else None
Le coût est de comparaisons pour le tri des valeurs de pièces, plus une opération par pièce rendue. La boucle while peut être remplacée par une division entière, nombre, montant = divmod(montant, piece), ce qui la rend tout compris.
Sur le système suisse, il est optimal
Le système suisse en circulation comporte les pièces de 5, 10 et 20 centimes, de 50 centimes, de 1, 2 et 5 francs, c'est-à-dire, exprimées en centimes, les valeurs , , , , , , — tout montant rendu est un multiple de 5 centimes depuis le retrait de la pièce de 1 centime. Sur ce système, l'algorithme glouton est optimal, et ce n'est pas une évidence: c'est une propriété du système, pas de l'algorithme.
La vérification est mécanique et sans appel: pour chaque montant multiple de 5 entre CHF 0,05 et CHF 100, on compare le nombre de pièces du glouton à celui que donne la programmation dynamique du chapitre 10. Sur les deux mille montants testés, les deux nombres coïncident toujours. Cela ne démontre rien pour CHF 12 500, mais la propriété se démontre par ailleurs, et le principe de la démonstration est un argument d'échange: on montre qu'une solution optimale ne peut contenir, pour chaque valeur de pièce, plus d'un certain nombre d'exemplaires — pas plus d'une pièce de 5 dans un rendu optimal, car deux pièces de 5 se remplacent par une de 10, et ainsi de suite — et l'on en déduit que la solution optimale est forcée d'être celle du glouton.
Sur d'autres systèmes, il échoue
Prenons un système fictif à trois valeurs: 1, 10 et 25. Pour rendre 30, le glouton prend d'abord 25, parce que c'est la plus grosse pièce qui tient; il lui reste 5 à rendre, et comme il n'a plus que des pièces de 1, il en aligne cinq. Six pièces. L'optimum est évidemment , trois pièces. Le glouton fait donc deux fois trop.
Ce n'est pas un accident isolé: sur les 99 montants de 1 à 99, ce système met le glouton en défaut trente fois — pour tous les montants de 30 à 34, de 40 à 44, de 55 à 59, de 65 à 69, de 80 à 84 et de 90 à 94. À chaque fois le mécanisme est le même: la grosse pièce est prise, et le reste ne se complète qu'avec de la petite monnaie.
Le détail troublant est qu'il suffit d'ajouter une pièce pour réparer le système. Avec les valeurs 1, 5, 10, 25 — le système américain — le glouton redevient optimal: sur les 999 montants de 1 à 999, aucun échec. Ajouter une possibilité rend donc l'algorithme meilleur, alors qu'il avait déjà toutes les anciennes à sa disposition. Une propriété de ce genre est le signe qu'on n'a pas affaire à une caractéristique de l'algorithme mais à une caractéristique arithmétique du système.
Le glouton échoue sur le système de pièces 1, 10 et 25 pour rendre 30. Que peut-on en conclure?
Cherchez le contre-exemple vous-même. Le programme contient déjà la version par programmation dynamique, qui rend le nombre minimal de pièces quel que soit le système. Écrivez la version gloutonne, puis faites chercher au programme le plus petit montant du système 1, 10, 25 sur lequel les deux ne sont pas d'accord; affichez ce montant, le nombre de pièces du glouton et celui de l'optimum.
«Ça a marché sur mes trois exemples»
Il reste à dire pourquoi ce chapitre insiste tant sur les démonstrations, dans une discipline où l'on peut exécuter son code.
Ce que les tests peuvent et ne peuvent pas
Un algorithme glouton est un objet trompeur: il est court, il est lisible, et il donne presque toujours une réponse plausible. Les trois critères réfutés dans ce chapitre — la durée la plus courte, la date de début la plus tôt, le rapport valeur/poids en version 0/1 — donnent tous l'optimum sur une majorité d'instances tirées au hasard. Le critère de la durée, sur l'instance témoin des huit tâches, trouve même exactement trois tâches, comme le bon critère; c'est en fabriquant délibérément la configuration en trois réunions qu'on le met à terre. Un jeu de tests écrit par la même personne que l'algorithme teste les cas auxquels cette personne a pensé, c'est-à-dire précisément ceux que son critère traite bien.
Il y a pire. Un algorithme glouton faux n'échoue pas bruyamment: il ne lève pas d'exception, il ne boucle pas, il ne rend pas une solution inadmissible. Il rend une solution parfaitement valide, simplement pas la meilleure — et sans l'optimum sous les yeux, rien ne le trahit. C'est le mode de défaillance le plus coûteux qui soit, parce qu'il est silencieux et qu'il peut durer des années.
Ce qu'il faut faire à la place
La discipline tient en trois gestes, dans cet ordre.
Chercher un contre-exemple, exhaustivement. Avant de démontrer, essayez de réfuter. Sur des instances de moins d'une quinzaine d'éléments, énumérez toutes les configurations possibles — ou tirez-en quelques millions au hasard sur un petit domaine — et comparez la sortie du glouton à l'optimum obtenu par force brute. Cette recherche est bon marché, elle s'écrit en dix lignes, et elle tranche: ou bien elle trouve un contre-exemple, et votre critère est mort; ou bien elle n'en trouve pas, et vous avez gagné le droit d'essayer de démontrer.
Ne pas confondre l'absence de contre-exemple avec une démonstration. Une recherche exhaustive sur les instances de taille n'exclut rien au-delà de 8, et il existe des critères gloutons qui ne fautent qu'à partir d'une taille appréciable. Un test qui ne trouve rien déplace une conjecture, il ne la démontre pas.
Écrire l'argument d'échange. C'est la seule chose qui ferme la question, et il a un avantage que les tests n'auront jamais: il vous dit pourquoi le critère marche. Rappelez-vous où le critère est entré dans la démonstration du théorème 9.1: une seule ligne, l'inégalité . Quand vous tentez le même argument avec le critère de la durée, cette ligne devient «la durée de est inférieure à celle de », qui est vraie — et parfaitement inutile, puisque ce dont l'échange a besoin est que au plus tôt, ce que la durée ne garantit pas. L'endroit exact où la démonstration refuse d'aboutir vous indique où chercher le contre-exemple: il suffit de construire une tâche courte qui finit tard.
Synthèse
- Un algorithme glouton construit sa solution par des choix locaux et irrévocables, selon un critère fixé. Il coûte typiquement un tri suivi d'un parcours, donc comparaisons dans le pire des cas, et il n'est optimal que si le problème possède deux propriétés: la propriété du choix glouton — il existe au moins une solution optimale contenant le premier choix — et la sous-structure optimale, partagée avec la programmation dynamique du chapitre 10.
- L'argument d'échange est la démonstration standard: partir d'une solution optimale quelconque, repérer le premier choix où elle diffère du glouton, l'y remplacer, vérifier que la solution reste admissible et que sa valeur ne se dégrade pas, conclure par récurrence. Les deux vérifications de la troisième étape sont indissociables, et c'est l'admissibilité qui échoue quand le critère est faux.
- L'ordonnancement d'intervalles se résout par la date de fin la plus tôt, avec démonstration: sur l'instance témoin des huit tâches, le glouton retient T1, T4 et T8, soit trois tâches, et c'est l'optimum. Deux critères plausibles sont faux, et chacun a son contre-exemple vérifié: la durée la plus courte place une seule réunion là où l'optimum en place deux (A 9h–13h, B 12h–14h, C 13h–17h), et la date de début la plus tôt en place une là où l'optimum en place trois.
- Le codage de Huffman fusionne à chaque étape les deux nœuds de plus petit poids. Sur l'instance du cours — , , , , , —, les cinq fusions , , , , donnent les codes , , , , , , soit contre 300 pour un code fixe de 3 bits, c'est-à-dire . L'optimalité se démontre par un lemme d'échange — les deux caractères les moins fréquents peuvent être frères à profondeur maximale — puis par récurrence sur l'alphabet réduit.
Exercices
Vous pouvez afficher le corrigé directement sous chaque énoncé après avoir cherché la solution.
On donne six tâches: , , , , , .
On considère l'alphabet avec les fréquences , , , , , soit 12 caractères au total.
- Effectuez les quatre fusions et donnez les poids des nœuds internes créés.
Capacité . Quatre objets: , , , .
On considère le système de pièces de valeurs .
- Que rend le glouton pour 8? Quel est l'optimum?
- Cherchez le plus petit montant sur lequel le glouton n'est pas optimal.
- Le système est-il canonique? Justifiez.
- Proposez une explication de ce qui rend défectueux.
Solution
1. Pour 8. Le glouton prend 6, puis ne peut plus que des 1: , . L'optimum est , .
Cet exercice demande une démonstration complète.
On considère le problème suivant, différent de celui du chapitre. On a tâches, la tâche ayant une durée ; une seule machine les exécute l'une après l'autre, sans interruption. Si l'ordre d'exécution est , la tâche placée en -ième position termine à la date . On veut minimiser la .
Références
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4ᵉ éd., MIT Press — chapitre 15, «Greedy Algorithms»: l'ordonnancement d'intervalles, les deux propriétés, le codage de Huffman avec le lemme d'échange et sa récurrence, et la comparaison explicite entre les deux versions du sac à dos.
- Cormen, Leiserson, Rivest & Stein, Algorithmique, 3ᵉ éd., Dunod — la traduction française du même chapitre, utile pour fixer le vocabulaire («choix glouton», «sous-structure optimale», «code préfixe»).
- D. Pearson, «A polynomial-time algorithm for the change-making problem», Operations Research Letters 33(3), 2005, p. 231–234 (DOI 10.1016/j.orl.2004.06.001) — l'article qui décide en si un système de pièces est canonique.
- Kleinberg & Tardos, Algorithm Design, Pearson — chapitre 4, la meilleure exposition disponible des deux formes de l'argument d'échange, avec la démonstration greedy stays ahead de l'ordonnancement d'intervalles et l'ordonnancement par échéances.
- Dasgupta, Papadimitriou & Vazirani, Algorithms, McGraw-Hill — chapitre 5, court et vif, avec Huffman et une discussion de l'entropie qui situe ce que le codage préfixe peut et ne peut pas atteindre.
- Sedgewick & Wayne, Algorithms, 4ᵉ éd., Addison-Wesley — section 5.5 pour la compression de données et l'implémentation complète de Huffman, table des codes comprise.
- Beauquier, Berstel & Chrétienne, Éléments d'algorithmique, Masson — pour le rendu de monnaie et la question des systèmes canoniques, traitée avec plus de soin que dans la plupart des manuels.