Adloun

Algorithmes gloutons

Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 7 · prépas scientifiques, tronc commun

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>7.1 Introduction et motivation

Le chapitre 6 s'est achevé sur un mur : énumérer toutes les solutions coûte ou , hors de portée dès que dépasse quelques dizaines. Comment choisir, alors, parmi un nombre astronomique de possibilités ? La stratégie la plus simple porte un nom gourmand : l'algorithme glouton (greedy) construit sa solution pas à pas en faisant, à chaque étape, le choix qui paraît localement le meilleur — la plus grosse pièce, l'activité qui finit le plus tôt — sans jamais revenir en arrière.

Cette stratégie a deux visages, et le chapitre les montre tous deux. Sur certains problèmes — la sélection d'activités, l'allocation de salles, le rendu de monnaie en euros — le glouton est non seulement rapide mais exactement optimal, et l'on saura le prouver : c'est l'argument d'échange, le troisième grand schéma de preuve du semestre après l'invariant et la récurrence. Sur d'autres — le rendu de monnaie dans un mauvais système, le sac à dos — le glouton se trompe, et il faut savoir exhiber le contre-exemple. La compétence visée n'est donc pas « appliquer le glouton », mais bien : proposer un critère glouton, et trancher, preuve ou contre-exemple à l'appui, s'il est correct.

7.2 Le principe glouton

Définition 7.1Algorithme glouton

Un algorithme glouton construit une solution par une suite de choix tels que :

  • chaque choix est fait selon un critère local (le meilleur candidat du moment, pour un certain ordre) ;
  • chaque choix est irrévocable : jamais l'algorithme ne revient sur une décision prise.

La structure typique : trier les candidats selon le critère, puis les examiner dans cet ordre en prenant chacun s'il est compatible avec les choix déjà faits.

iRemarque

Le glouton est rapide par construction : un tri (, chapitre 9) suivi d'un parcours (). Toute la question est ailleurs : la suite de choix localement bons est-elle globalement optimale ? En général, non — gravir la pente la plus raide ne mène pas au plus haut sommet. Mais sur des problèmes bien structurés, oui, et c'est démontrable.

7.3 Le rendu de monnaie

7.3.1 L'algorithme

Exemple 7.2Rendre la monnaie en euros

Rendre euros avec le moins de pièces et billets possible : tout le monde fait pareil — un billet de , une pièce de , deux de . C'est le glouton : à chaque étape, la plus grande valeur qui ne dépasse pas le restant. En centimes, avec le système euro (on s'arrête au billet de 5 pour l'exemple) :


def rendu_monnaie(montant: int, systeme: list) -> list:
    """Renvoie la liste des valeurs rendues (avec répétitions).
    Préconditions : montant >= 0, systeme trié décroissant et contenant 1."""
    rendu = []
    for piece in systeme:
        # Invariant : montant = montant initial - somme des pièces de rendu,
        #             et aucune pièce > piece ne peut plus servir
        while montant >= piece:
            rendu.append(piece)
            montant -= piece
    return rendu

assert rendu_monnaie(640, [500, 200, 100, 50, 20, 10, 5, 2, 1]) \
       == [500, 100, 20, 20]
assert rendu_monnaie(0, [500, 200, 100, 50, 20, 10, 5, 2, 1]) == []

Terminaison : à chaque tour de la boucle interne, montant décroît strictement (variant) ; la précondition « le système contient » garantit qu'on atteint toujours — sans elle, rendu_monnaie(3, [2]) laisserait un reste. Coût : pour valeurs et pièces rendues.

7.3.2 Optimal ici, faux ailleurs

ImportantLe glouton du rendu de monnaie n'est pas toujours optimal

Tout dépend du système de pièces. Avec le système et le montant :

Le choix local « prendre » paraît le meilleur — il laisse le plus petit reste — mais il interdit la décomposition : voilà, sur quatre nombres, toute la faillibilité du glouton. Les systèmes pour lesquels le glouton est optimal pour tout montant sont dits canoniques ; le système de l'euro l'est (admis — la preuve, valeur par valeur, est un exercice), le système ne l'est pas.

Méthode : Que faire face à un glouton douteux ?

Devant un critère glouton proposé, deux issues, et une seule démarche :

  • chercher un petit contre-exemple (souvent ou éléments suffisent — qu'on peut traquer par énumération exhaustive du chapitre 6 sur de petites instances : comparer le glouton à la solution optimale énumérée) ;
  • si aucun contre-exemple ne vient, tenter la preuve d'échange (section suivante).

Affirmer l'optimalité sans preuve, ou la nier sans contre-exemple, sont deux fautes égales.

7.4 La sélection d'activités

7.4.1 Le problème et le bon critère

Définition 7.3Sélection d'activités

On dispose d'une salle et de activités, chacune avec une heure de début et une heure de fin . Deux activités sont compatibles si leurs intervalles ne se chevauchent pas. Objectif : sélectionner un ensemble d'activités deux à deux compatibles, de cardinal maximal.

Exemple 7.4Trois critères candidats, deux pièges

Trois critères gloutons plausibles :

  • la plus courte d'abord ? Contre-exemple : , , — la courte bloque les deux longues : glouton activité, optimal .
  • celle qui commence le plus tôt ? Contre-exemple : , , — la lève-tôt occupe tout : glouton , optimal .
  • celle qui finit le plus tôt : aucune de ces embûches — et pour cause : c'est le critère optimal, comme on va le prouver.
Définition 7.5Algorithme glouton par date de fin

def selection_activites(activites: list) -> list:
    """activites : liste de couples (debut, fin). Renvoie une sélection
    de cardinal maximal d'activités deux à deux compatibles."""
    tri = sorted(activites, key=lambda a: a[1])     # par date de FIN croissante
    choisies = []
    fin_courante = None
    for (d, f) in tri:
        # Invariant : choisies est compatible, et fin_courante est la
        # date de fin de la dernière activité choisie
        if fin_courante is None or d >= fin_courante:
            choisies.append((d, f))
            fin_courante = f
    return choisies

a = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (6, 10), (8, 11)]
assert selection_activites(a) == [(1, 4), (5, 7), (8, 11)]

Coût : le tri en , puis un parcours en .

7.4.2 La preuve d'échange

◆Théorème 7.6Optimalité du glouton par date de fin

La sélection produite par l'algorithme ci-dessus est de cardinal maximal.

Démonstration

Notons les activités choisies par le glouton, dans l'ordre, et soit une sélection optimale, rangée par dates de fin croissantes. Montrons d'abord, par récurrence sur , que le glouton finit toujours au plus tard en même temps : pour tout .

Base : est, parmi toutes les activités, l'une de celles qui finissent le plus tôt — en particulier . Hérédité : supposons . L'activité commence après : elle était donc compatible avec les choix du glouton au moment où celui-ci a choisi , et disponible. Le glouton ayant pris l'activité compatible qui finit le plus tôt, . ✓

Supposons alors . L'activité commence après : elle est compatible avec toute la sélection gloutonne — mais alors le glouton, qui parcourt toutes les activités, l'aurait acceptée (elle ou une autre) comme -ième choix : contradiction avec le fait qu'il s'est arrêté à . Donc : le glouton est optimal. (Le cœur de l'argument : « le choix glouton ne ferme aucune porte » — toute solution optimale peut être échangée, activité par activité, contre la solution gloutonne sans perdre en cardinal. C'est le schéma de preuve à retenir, et à réutiliser tel quel pour les salles.)

7.5 L'allocation de salles

Définition 7.7Le problème

Mêmes activités, mais cette fois toutes doivent avoir lieu : combien faut-il de salles au minimum, et comment les attribuer ? (C'est le problème de l'emploi du temps — ou des quais de gare, ou des machines.)

Exemple 7.8Glouton d'affectation

On traite les cours par début croissant, en réutilisant une salle libre dès que possible :


def allocation_salles(activites: list) -> list:
    """Renvoie une liste de salles, chacune étant la liste de ses activités.
    Le nombre de salles renvoyé est minimal."""
    tri = sorted(activites)               # par DÉBUT croissant
    salles = []                           # salles[i] : liste d'activités ; on
    fins = []                             # mémorise la fin de la dernière de chacune
    for (d, f) in tri:
        # Invariant : les activités déjà placées sont compatibles dans chaque salle
        placee = False
        for i in range(len(salles)):
            if fins[i] <= d:              # la salle i est libre à l'instant d
                salles[i].append((d, f))
                fins[i] = f
                placee = True
                break
        if not placee:                    # aucune salle libre : on en ouvre une
            salles.append([(d, f)])
            fins.append(f)
    return salles
Démonstration (Optimalité)

Disons que l'algorithme ouvre salles, et considérons l'instant où la -ième est ouverte : l'activité qui arrive commence en et aucune des salles n'est libre — chacune héberge une activité qui a commencé avant (tri par débuts) et finit après . Il existe donc activités simultanément en cours à l'instant . Or deux activités simultanées ne peuvent partager une salle : toute solution utilise au moins salles. Notre allocation en utilise exactement : elle est minimale. (La preuve exhibe un certificat : activités deux à deux incompatibles. Le nombre minimal de salles est ainsi coincé entre notre solution, qui en donne , et le certificat, qui en exige — encadrement parfait, et l'occasion de noter que la borne inférieure se lit sur le problème, pas sur l'algorithme.)

iRemarque

La recherche d'une salle libre par parcours coûte par activité, d'où au pire — quadratique si presque tout se chevauche. Une structure adaptée (file de priorité sur les dates de fin, second semestre) ramène le tout à . L'idée gloutonne ne change pas, seule l'intendance s'améliore : idée et implémentation sont deux étages distincts de l'analyse.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>7.6 Exercices résolus

Niveau (Application directe du cours)

Exercice 1 : Rendus de monnaie, à la main et à la machine

Donner le rendu glouton de euros (en centimes : ) dans le système euro complet , vérifier avec la fonction du cours, et compter les pièces. Le glouton peut-il rendre avec moins de pièces ? (On admettra le caractère canonique du système euro.)

Démonstration (Solution)

À la main : — cinq pièces (, , , , ). La machine confirme : rendu_monnaie(263, [200, 100, 50, 20, 10, 5, 2, 1]) renvoie [200, 50, 10, 2, 1]. Le système euro étant canonique (admis), ce rendu de pièces est optimal : aucune décomposition de en valeurs du système n'utilise pièces ou moins. (Petit contrôle de cohérence indépendant : avec pièces au plus et une valeur maximale de , on atteint au mieux — l'argument grossier ne suffit donc pas à conclure, et c'est bien la canonicité qui travaille.)

Exercice 2 : Le contre-exemple à construire soi-même

Dans le système , trouver le plus petit montant pour lequel le glouton n'est pas optimal, en donnant les deux décompositions.

Démonstration (Solution)

Testons les montants croissants. Pour , le glouton n'utilise que et : est clairement optimal (une seule façon raisonnable). Pour : glouton ( pièces) ; or ( pièces) — non optimal, et les montants se vérifient un à un favorables au glouton ( ; ; contre : le glouton gagne ; contre : le glouton gagne). Le plus petit contre-exemple est donc . (La méthode vaut mieux que le résultat : énumérer les petits montants, comparer glouton et optimal — à la main ici, par programme dès que le système grossit ; c'est l'exercice 27 de la banque.)

Exercice 3 : Dérouler la sélection d'activités

Sept activités : , , , , , , . Dérouler l'algorithme glouton (tri par fin, parcours) et donner la sélection — puis vérifier qu'aucune sélection de cardinal supérieur n'existe.

Démonstration (Solution)

Tri par date de fin : , , , , , , . Parcours : prise (fin courante ) ; rejetée () ; prise ( — bornes : la fin est exclue, commence quand s'achève ; fin courante ) ; rejetée () ; prise (, fin ) ; rejetée () ; prise (). Sélection : , cardinal .

Optimalité : par le théorème du cours, le glouton est optimal — mais vérifions par certificat direct : à tout choix de activités, le principe des tiroirs appliqué aux quatre créneaux , , , … est ici inutilement subtil ; le plus simple est d'observer que les fins s'étalent de à et que cinq intervalles disjoints exigeraient une durée totale d'au moins dans , ce qui ne conclut pas non plus — la preuve générale du cours est, de fait, le bon argument, et c'est précisément sa valeur : elle dispense de l'examen cas par cas. (Honnêteté de méthode : quand le certificat ad hoc devient laborieux, c'est le théorème général qu'il faut citer.)

Niveau (Application avec raisonnement intermédiaire)

Exercice 4 : Combien de salles pour l'emploi du temps ?

Avec les sept activités de l'exercice 3, dérouler l'allocation de salles, donner le nombre de salles obtenu, et exhiber le certificat d'optimalité (l'instant critique et ses activités simultanées).

Démonstration (Solution)

Tri par début : , , , , , , — le tri lexicographique de sorted départage les débuts égaux par la fin : passe avant , avant . Parcours : salle 1 (fin ) ; : salle 1 occupée () salle 2 (fin ) ; : salle 1 libre () salle 1 (fin ) ; : salle 1 () non, salle 2 () non salle 3 (fin ) ; : salle 1 libre () salle 1 (fin ) ; : salle 1 () non, salle 2 () oui salle 2 (fin ) ; : salle 1 () salle 1. Trois salles.

Certificat : à l'instant par exemple, , et sont toutes trois en cours — trois activités deux à deux incompatibles : toute solution exige au moins salles. Le glouton en utilise : optimal. (Le déroulé montre l'instant exact où la troisième salle s'ouvre — l'arrivée de — et c'est bien là que vit le certificat, comme dans la preuve générale.)

Exercice 5 : Le glouton fractionnaire qui a raison

Un randonneur dispose d'un sac de capacité kg et de denrées divisibles : kg de valeur , kg de valeur , kg de valeur . Quel est le critère glouton correct, et quelle valeur maximale emporte-t-il ? Montrer que le critère « plus grande valeur d'abord » est, lui, sous-optimal.

Démonstration (Solution)

Quand on peut fractionner, le bon critère est la valeur au kilo : , , . Tri décroissant : la denrée à /kg ( kg), puis celle à /kg ( kg), puis celle à /kg. Le sac prend kg (valeur ), puis kg (valeur ) — il est plein : valeur totale .

Critère « plus grande valeur d'abord » : on prend les kg de valeur , puis kg sur les kg de valeur (fraction , valeur ) : total — sous-optimal.

L'optimalité de la valeur au kilo se voit par échange : si une solution emporte du poids d'une denrée moins dense alors qu'il reste de la plus dense, remplacer un gramme de l'une par un gramme de l'autre augmente la valeur — toute solution optimale remplit donc le sac par densités décroissantes, ce que fait le glouton. (Retenir le distinguo, qui prépare l'exercice 8 : fractionnable le glouton par densité est optimal ; indivisible il ne l'est plus.)

Exercice 6 : Minimiser l'attente totale

Au guichet, clients ont des durées de service connues. L'ordre de passage change la somme des temps d'attente (chacun attend la fin de tous ses prédécesseurs). Quel ordre la minimise ? Le prouver par échange.

Démonstration (Solution)

Critère : servir par durées croissantes (le plus court d'abord). Intuition : la durée du premier client est subie par les suivants, celle du deuxième par , etc. — la somme des attentes vaut : il faut affecter les grands coefficients aux petites durées.

Preuve d'échange : soit un ordre optimal où deux clients consécutifs vérifient ( passe juste avant ). Échangeons-les : seules leurs attentes mutuelles changent — avant l'échange, attend de plus ; après, attend de plus ; la somme varie de : elle diminue strictement, contredisant l'optimalité. Donc dans tout ordre optimal, les durées consécutives sont croissantes — l'ordre croissant est optimal. (L'argument « si deux voisins violent le critère, les échanger améliore » est la forme la plus pure de la preuve d'échange ; on remarquera son parfum de tri à bulles — corriger les inversions du critère — ce qui n'est pas un hasard : trier selon le bon critère est l'algorithme.)

Exercice 7 : Les bidons d'essence

Une voiture parcourt un circuit de stations-service ; elle peut parcourir kilomètres par plein, et les stations sont aux positions croissantes (l'arrivée est ). Donner le glouton qui minimise le nombre d'arrêts, le prouver, et préciser la condition de faisabilité.

Démonstration (Solution)

Faisabilité : il faut pour tout — sinon un tronçon est infranchissable, quel que soit l'algorithme.

Glouton : partir le plein fait, et à chaque arrêt, aller le plus loin possible — s'arrêter à la dernière station atteignable.


def arrets(p: list, K: int) -> list:
    """p : positions croissantes, p[0] = 0 = départ, p[-1] = arrivée.
    Renvoie les positions des arrêts (hors départ/arrivée)."""
    stops, position = [], 0
    i = 0
    while p[i] != p[-1]:
        # aller à la dernière station accessible depuis 'position'
        while i + 1 < len(p) and p[i + 1] - position <= K:
            i += 1
        stops.append(p[i])
        position = p[i]
    return stops[:-1] if stops and stops[-1] == p[-1] else stops

Preuve (échange, version « le glouton domine ») : notons les positions des arrêts gloutons et ceux d'une solution optimale. Par récurrence, pour tout : au premier arrêt, le glouton va par définition aussi loin que possible, donc ; et si , le glouton repart d'au moins aussi loin et choisit de nouveau le point accessible le plus lointain, donc . Le glouton, toujours en avance, atteint l'arrivée avec au plus autant d'arrêts. (Variante du schéma d'échange dite « le glouton reste devant » — la même qui prouvait pour les activités : repérer cette structure de preuve dans les deux cas est tout l'enjeu du chapitre.)

Niveau (Raisonnement subtil ou plusieurs étapes)

Exercice 8 : Le sac à dos indivisible, ou la chute du glouton

Sac de capacité kg, objets indivisibles : , , — les mêmes qu'à l'exercice 5. Montrer que les trois critères gloutons naturels (valeur, densité, poids croissant) sont tous sous-optimaux ici, et trouver l'optimum par énumération des sous-ensembles (chapitre 6).

Démonstration (Solution)

Les sous-ensembles tenant dans kg : ; ; ; kg, valeur ; kg, valeur ; kg : trop lourd ; : trop lourd. Optimum : avec .

Les gloutons : par valeur — prend , puis ne tient pas, puis tient : , valeur … optimal ici ! Construisons alors mieux : remplaçons l'objet 2 par . Énumération : ; ; … optimum toujours . Par valeur : prend puis ne tient pas puis : — encore optimal. Le contre-exemple demande un peu plus de soin : capacité , objets , , . Par valeur et par densité ( ; ; ) : prennent , plus rien ne tient — valeur ; par poids croissant : … qui est l'optimum (l'énumération le confirme : , , sont les seules valeurs atteignables). Donc valeur et densité échouent (). Pour faire chuter aussi le poids croissant : capacité , objets , , — poids croissant prend puis : ; l'optimum est . Aucun des trois critères n'est correct en général. (Le sac à dos indivisible est un problème difficile au sens fort — comme la somme de sous-ensemble du chapitre 6, dont il est cousin ; le glouton y sert de solution approchée, jamais garantie. La frontière exacte avec l'exercice 5 tient en un mot : la divisibilité — qui autorise l'argument d'échange « gramme contre gramme », impossible sur des objets entiers.)

Exercice 9 : Le photographe de classe

élèves ont des tailles ; on doit les ranger sur une ligne de emplacements de hauteurs (un élève par emplacement), en minimisant le plus grand écart . Montrer que l'appariement glouton « trier les deux listes et apparier par rang » est optimal.

Démonstration (Solution)

Supposons les deux listes triées : et ; le glouton apparie avec . Soit un appariement optimal quelconque : s'il contient un croisement — deux indices avec appariés à — décroisons-les (apparier et ). Vérifions que le maximum des deux écarts ne croît pas : en notant les tailles et les hauteurs, il s'agit de voir

Or les quatre points et sur la droite réelle vérifient : (si , alors ; sinon ) et de même pour . ✓ Chaque décroisement supprime au moins une inversion entre les deux listes sans augmenter le critère : en itérant (le nombre d'inversions est un variant !), on atteint l'appariement croissant du glouton, de critère au plus égal — il est optimal. (Trois chapitres se rejoignent : la preuve d'échange du jour, le variant du chapitre 1, et les inversions du chapitre 3. L'inégalité centrale — « apparier dans l'ordre n'aggrave jamais le pire écart » — resservira sous d'autres habits, des files d'attente aux statistiques.)

Exercice 10 : Tester un glouton contre la force brute

Écrire un banc d'essai qui compare selection_activites à l'optimum calculé par énumération exhaustive (chapitre 6) sur des milliers de petites instances aléatoires — puis l'appliquer au critère faux « la plus courte d'abord » pour mesurer sa fréquence d'échec et son écart moyen à l'optimum.

Démonstration (Solution)

import random

def compatibles(sel: list) -> bool:
    s = sorted(sel)
    return all(s[i][1] <= s[i + 1][0] for i in range(len(s) - 1))

def optimum_brut(activites: list) -> int:
    meilleur = 0
    for sous in sous_listes(activites):          # chapitre 6 : 2**n candidats
        if compatibles(sous):
            meilleur = max(meilleur, len(sous))
    return meilleur

def plus_courte_d_abord(activites: list) -> list:
    tri = sorted(activites, key=lambda a: a[1] - a[0])
    choisies = []
    for (d, f) in tri:
        if all(f <= d2 or f2 <= d for (d2, f2) in choisies):
            choisies.append((d, f))
    return choisies

random.seed(7)
echecs, ecart_total, essais = 0, 0, 2000
for _ in range(essais):
    acts = []
    for _ in range(random.randint(1, 8)):        # petites instances : 2**8 max
        d = random.randint(0, 20)
        acts.append((d, d + random.randint(1, 6)))
    opt = optimum_brut(acts)
    assert len(selection_activites(acts)) == opt          # JAMAIS d'écart
    courte = len(plus_courte_d_abord(acts))
    if courte < opt:
        echecs += 1
        ecart_total += opt - courte
print(echecs / essais, ecart_total / max(echecs, 1))
#  2 % d'échecs (39 sur 2000 avec cette graine), écart moyen 1 quand il y a échec

Le banc confirme expérimentalement le théorème — aucun écart pour le critère par date de fin sur instances — et chiffre la faillite du critère naïf : il échoue sur une petite fraction des instances — environ une sur cinquante ici —, généralement d'une activité ; rare ne veut pas dire correct : un seul échec suffit à réfuter. (Trois usages de ce banc : conjecturer (un glouton qui survit à instances aléatoires mérite qu'on tente la preuve), réfuter (le premier échec est un contre-exemple, que le banc peut afficher), et mesurer (un glouton faux peut rester une bonne heuristique — décision d'ingénieur, à prendre chiffres en main). La force brute exponentielle, inutilisable en production, trouve ici son vrai métier : juge de paix sur petites instances.)

Synthèse du chapitre (à retenir)
  • Glouton : trier selon un critère local, choisir irrévocablement chaque candidat compatible ; coût typique — la rapidité est garantie, l'optimalité jamais a priori.
  • La démarche : critère proposé chercher un petit contre-exemple (à la main ou par banc force-brute sur petites instances) ; s'il résiste preuve d'échange.
  • Preuve d'échange, trois variantes : « le glouton reste devant » (activités : ; essence : ) ; « échanger deux voisins qui violent le critère améliore » (guichet, photographe — avec le nombre d'inversions comme variant) ; « certificat » (salles : activités simultanées au moins salles — l'optimum est coincé).
  • Rendu de monnaie : glouton optimal sur les systèmes canoniques (euro : admis), faux ailleurs — : contre ; : premier échec à .
  • Sélection d'activités : critère correct date de fin croissante (plus courte d'abord et début au plus tôt : contre-exemples) ; salles : tri par débuts, réutiliser une salle libre, optimal par certificat.
  • Sac à dos : fractionnable densité (valeur/poids) optimale par échange gramme à gramme ; indivisible tous les critères naïfs ont des contre-exemples — problème difficile, glouton heuristique.
  • Le banc force-brute (énumérations du chapitre 6 sur ) : conjecturer, réfuter (premier échec contre-exemple), mesurer la qualité d'une heuristique.

7.7 Exercices d'entraînement

Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.

A. Rendu de monnaie

B. Activités et planification

C. Preuves d'échange

D. Quand le glouton échoue — et que faire

Continuer sur Adloun : animation, QCM, fiches, exercices