CPGE 2ᵉ année — MP / PSI · Informatique

Les algorithmes gloutons

Pr. EL HADIQ Zouhair

Une stratégie de résolution des problèmes d'optimisation : à chaque étape, faire le choix qui semble le meilleur sur le moment, sans jamais revenir en arrière. Simple et rapide — mais pas toujours optimale.

🎯 Objectifs du chapitre
Sommaire
  1. Problèmes d'optimisation et idée gloutonne
  2. Structure d'un algorithme glouton (choix glouton et sous-structure optimale)
  3. Le rendu de monnaie
  4. Le problème du sac à dos
  5. L'arbre de Huffman (codage optimal)
  6. Quand le glouton fonctionne-t-il ? — récapitulatif

1. Problèmes d'optimisation et idée gloutonne

Un problème d'optimisation consiste à trouver, parmi un ensemble (souvent énorme) de solutions possibles, une solution qui maximise ou minimise une certaine quantité, appelée fonction objectif. Exemples : rendre une somme avec le moins de pièces possible, remplir un sac pour maximiser la valeur transportée, coder un texte avec le moins de bits possible.

Explorer toutes les solutions (force brute) est en général trop coûteux : le nombre de combinaisons explose exponentiellement. On cherche donc des méthodes plus malines. L'approche gloutonne (en anglais greedy, « gourmande ») est l'une des plus simples :

Principe glouton. On construit la solution pas à pas. À chaque étape, on effectue le choix qui paraît localement le meilleur (le plus « gourmand ») selon un critère simple, sans jamais remettre en cause les choix déjà faits.

L'algorithme est donc rapide et facile à écrire. Sa faiblesse est dans le mot « localement » : un enchaînement de choix optimaux sur le moment ne donne pas forcément la meilleure solution globale. Tout l'enjeu du chapitre est de distinguer les cas où le glouton est correct de ceux où il échoue.

2. Structure d'un algorithme glouton

Un algorithme glouton suit toujours le même schéma : tant que la solution n'est pas complète, choisir le « meilleur » candidat restant et l'ajouter s'il est compatible.

# Schéma général d'un algorithme glouton
def glouton(candidats):
    solution = []
    while not complet(solution) and candidats:
        x = meilleur_candidat(candidats)   # choix glouton
        candidats.remove(x)
        if compatible(solution, x):
            solution.append(x)
    return solution

Pour qu'un tel algorithme produise une solution optimale, le problème doit posséder deux propriétés.

1. Propriété du choix glouton. Il existe une solution optimale qui contient le choix localement optimal effectué à la première étape. Autrement dit : faire le choix glouton ne « ferme jamais la porte » à l'optimum.
2. Sous-structure optimale. Une solution optimale du problème contient en elle des solutions optimales des sous-problèmes. Une fois le premier choix fait, il reste un problème de même nature, plus petit, qu'on résout par le même procédé.

La sous-structure optimale est aussi le fondement de la programmation dynamique (chapitre suivant). La différence : le glouton fait un seul choix — celui qui paraît le meilleur — et ne le remet jamais en cause ; la programmation dynamique examine plusieurs choix et garde le meilleur. Le glouton est donc plus rapide, mais correct seulement lorsque la propriété du choix glouton est vérifiée.

⚠️ Point de vigilance. Démontrer qu'un glouton est optimal demande une vraie preuve (souvent par échange : on transforme une solution optimale quelconque en la solution gloutonne sans dégrader l'objectif). À l'inverse, montrer qu'un glouton n'est pas optimal est facile : il suffit d'un seul contre-exemple.

3. Le rendu de monnaie

Problème. On dispose d'un système de pièces (par exemple 1, 2, 5, 10, 20, 50 centimes…) en quantité illimitée. Pour rendre une somme S, on veut utiliser le nombre minimal de pièces.

Stratégie gloutonne. À chaque étape, prendre la plus grande pièce qui ne dépasse pas la somme restante, puis recommencer.

def rendu_monnaie(systeme, S):
    # systeme : pièces triées par ordre décroissant
    rendu = []
    for piece in systeme:
        while S >= piece:
            rendu.append(piece)
            S = S - piece
    return rendu

Pour rendre 68 centimes avec (50, 20, 10, 5, 2, 1) : on prend 50, puis 10, puis 5, puis 2, puis 1, soit 5 pièces : 50 + 10 + 5 + 2 + 1 = 68. C'est bien optimal.

✅ Le glouton est optimal… pour les bons systèmes. Pour le système de pièces de l'euro (et celui de la plupart des monnaies réelles), dit canonique, l'algorithme glouton donne toujours le nombre minimal de pièces.
❌ Contre-exemple (système non canonique). Avec le système (1, 3, 4) et S = 6 : le glouton prend 4, puis 1, puis 1, soit 3 pièces (4 + 1 + 1). Or la solution optimale est 2 pièces : 3 + 3 ! Le choix glouton « prendre 4 d'abord » a fermé la porte à l'optimum. Ce système ne vérifie pas la propriété du choix glouton.

Conclusion : le rendu de monnaie illustre parfaitement la nuance du chapitre. Le même algorithme glouton est optimal pour certains systèmes et sous-optimal pour d'autres. Pour un système quelconque, il faut la programmation dynamique pour garantir l'optimum.

4. Le problème du sac à dos

Problème. On dispose d'objets, chacun ayant un poids pi et une valeur vi, et d'un sac de capacité maximale C. On veut emporter un ensemble d'objets de valeur totale maximale sans dépasser C.

Il existe deux versions, et c'est précisément là que se joue la différence.

4.1 — Sac à dos fractionnaire

On peut prendre des fractions d'objet (par exemple de la poudre d'or, du sable, un liquide). La stratégie gloutonne est : trier les objets par valeur par unité de poids (vi / pi) décroissante, puis remplir le sac en prenant d'abord les objets les plus « rentables », et terminer en prenant une fraction du dernier objet pour combler exactement la capacité.

def sac_fractionnaire(objets, C):
    # objets : liste de (valeur, poids)
    objets = sorted(objets, key=lambda o: o[0] / o[1], reverse=True)
    valeur = 0.0
    for v, p in objets:
        if C >= p:                 # on prend l'objet entier
            valeur += v; C -= p
        else:                       # on prend une fraction et on s'arrête
            valeur += v * (C / p); C = 0
            break
    return valeur
✅ Le glouton est optimal. Pour le sac fractionnaire, prendre toujours l'objet le plus rentable au kilo est prouvablement optimal (preuve par échange). Le problème vérifie la propriété du choix glouton.

4.2 — Sac à dos 0/1

Cette fois, chaque objet est indivisible : on le prend en entier (1) ou pas du tout (0). Le même glouton « le plus rentable d'abord » n'est plus optimal.

❌ Contre-exemple (sac à dos 0/1). Capacité C = 10. Trois objets :
Objet Valeur Poids Valeur / poids
A60106
B100205
C120304
Avec C = 50 : le glouton prend A (rentabilité 6, poids 10), puis B (poids 20, total 30), puis ne peut plus prendre C (il resterait 20 < 30). Valeur = 60 + 100 = 160. Or prendre B + C (poids 20 + 30 = 50) donne une valeur de 220 ! Le glouton, en se jetant sur l'objet le plus rentable, rate la meilleure combinaison.

Le sac à dos 0/1 ne vérifie pas la propriété du choix glouton : il faut la programmation dynamique (chapitre suivant) pour le résoudre exactement. C'est l'exemple canonique d'un problème où « gourmand » ne rime pas avec « optimal ».

5. L'arbre de Huffman (codage optimal)

Problème. On veut compresser un texte. Plutôt que de coder chaque caractère sur le même nombre de bits (codage à longueur fixe), on donne des codes courts aux caractères fréquents et des codes longs aux caractères rares. Pour pouvoir décoder sans ambiguïté, aucun code ne doit être le préfixe d'un autre : c'est un code préfixe, représenté par un arbre binaire dont les feuilles sont les caractères (gauche = 0, droite = 1).

Algorithme de Huffman (glouton). On part d'une forêt où chaque caractère est un arbre isolé, pondéré par sa fréquence. Tant qu'il reste plus d'un arbre :

  1. choisir les deux arbres de plus petite fréquence (le choix glouton) ;
  2. les fusionner sous une nouvelle racine, de fréquence égale à la somme des deux ;
  3. replacer ce nouvel arbre dans la forêt.

Le dernier arbre restant est l'arbre de Huffman. On lit le code de chaque caractère en descendant de la racine jusqu'à sa feuille.

import heapq

def huffman(freq):
    # freq : dict {caractère: fréquence}
    tas = [[f, c, None, None] for c, f in freq.items()]
    heapq.heapify(tas)
    while len(tas) > 1:
        g = heapq.heappop(tas)          # plus petite fréquence
        d = heapq.heappop(tas)          # deuxième plus petite
        heapq.heappush(tas, [g[0] + d[0], None, g, d])
    return tas[0]          # racine de l'arbre de Huffman

Exemple. Fréquences A:5, B:2, C:1, D:1. On fusionne d'abord C et D (1 + 1 = 2), puis ce nœud avec B (2 + 2 = 4), puis avec A (4 + 5 = 9). On obtient les codes A:0, B:11, C:100, D:101 — les caractères fréquents (A) sont sur 1 bit, les rares (C, D) sur 3 bits.

✅ Le glouton de Huffman est optimal. On démontre que fusionner les deux fréquences les plus faibles conduit toujours à un code préfixe de longueur moyenne minimale (propriété du choix glouton + sous-structure optimale). C'est l'un des grands succès de l'approche gloutonne, utilisé dans de vrais formats de compression (JPEG, ZIP, MP3…).

6. Quand le glouton fonctionne-t-il ? — récapitulatif

Problème Choix glouton Glouton optimal ?
Rendu de monnaie (système canonique, ex. euro) Plus grande pièce \(\le\) reste Oui
Rendu de monnaie (système quelconque, ex. 1,3,4) Plus grande pièce \(\le\) reste Non (6 = 3+3)
Sac à dos fractionnaire Meilleure valeur/poids d'abord Oui
Sac à dos 0/1 Meilleure valeur/poids d'abord Non
Codage de Huffman Fusionner les 2 plus petites fréquences Oui
✅ Points essentiels.

CPGE MP/PSI — Informatique · Chapitre : Les algorithmes gloutons · Conforme au programme officiel 2023