Introduction aux algorithmes gloutons

01 May 2019 01 May 2019 10643 vues ESSADDOUKI Mostafa 13 min de lecture

Les algorithmes gloutons — Greedy Algorithms

Introduction et principe fondamental

Définition — Algorithme glouton Un algorithme glouton (greedy algorithm) adopte une stratégie simple mais puissante :

« À chaque étape, il choisit le meilleur choix possible selon un critère local, sans jamais remettre en question les choix précédents. »

L'algorithme avance pas à pas, en effectuant des choix immédiats qui semblent les plus avantageux sur le moment — d'où le terme « glouton » (celui qui « mange » tout de suite ce qui paraît le meilleur).

Ce principe repose sur l'idée qu'un bon choix local conduit à une solution globale optimale, ce qui est vrai pour certains problèmes… mais pas pour tous.

 Exemple — Chemin de somme maximale

Dans l'arbre binaire ci-dessous, trouver le chemin allant de la racine à une feuille qui donne la plus grande somme des valeurs des nœuds traversés.

Arbre binaire
            8
           / \
          2   5
         / \ / \
        21 12 16 12

Approche gloutonne — à chaque nœud, choisir le fils de plus grande valeur :

  • À la racine (8) : 5 > 2 → on choisit le fils droit 5
  • Au nœud (5) : 16 > 12 → on choisit le fils gauche 16

Chemin glouton : 8 → 5 → 16 — Somme = 29

Solution optimale — en examinant tous les chemins :

CheminCalculSomme
8 → 2 → 218 + 2 + 2131 ← Optimal
8 → 2 → 128 + 2 + 1222
8 → 5 → 168 + 5 + 1629 ← Glouton ✗
8 → 5 → 128 + 5 + 1225
L'algorithme glouton échoue sur cet exemple ! Le chemin optimal 8 → 2 → 21 (somme = 31) n'est pas trouvé car l'algorithme glouton se base uniquement sur des informations locales sans considérer l'impact global des décisions.

Bien que le nœud 5 soit plus grand que le nœud 2, ce dernier donne accès à la feuille 21 qui compense largement son désavantage initial. Un sacrifice à court terme peut mener à un bénéfice supérieur à long terme.

 Exemple — Rendu de monnaie

On doit rendre 63 DH avec des pièces de valeurs : 1, 2, 5, 10, 20 DH. Stratégie gloutonne : toujours prendre la plus grande pièce ≤ montant restant.

Reste à rendrePièce choisieNombre de pièces
63 DH20 DH1
43 DH20 DH2
23 DH20 DH3
3 DH2 DH4
1 DH1 DH5 ← Optimal ✓
Contre-exemple — système {1, 3, 4}, rendre 6 DH

Approche gloutonneSolution optimale
Décomposition4 + 1 + 1 = 63 + 3 = 6
Nombre de pièces3 pièces ✗2 pièces ✓

Ce contre-exemple montre que la stratégie gloutonne n'est pas universellement optimale. Quand elle échoue, on recourt à la programmation dynamique.

Structure générale d'un algorithme glouton

Les 3 étapes
  1. Initialiser la solution (souvent vide).
  2. Répétertant que la solution n'est pas complète :
    • Sélectionner le meilleur choix local selon un critère précis (le plus petit coût, la plus grande valeur, le plus court temps, etc.).
    • Vérifier si ce choix est compatible avec les choix déjà effectués.
    • Ajouter ce choix à la solution.
  3. Retourner la solution construite.
   
Python — Patron général (v1)
def glouton(donnees):
    solution = []
    while not solution_complete(solution):
        choix = meilleur_choix(donnees, solution)
        if choix_valide(choix, solution):
            solution.append(choix)
    return solution
Rôle crucial du tri Une fois les données triées selon le bon critère, il suffit de les parcourir séquentiellement et de sélectionner les éléments respectant les conditions du problème. L'essentiel du travail algorithmique consiste donc à identifier le bon critère de tri.

Complexité typique : O(n log n) pour le tri + O(n) pour le parcours = O(n log n) au total.
   
Python — Patron général (v2 — avec tri)
def algorithme_glouton(donnees):
    # ÉTAPE 1 : TRI selon le critère glouton — O(n log n)
    donnees_triees = sorted(donnees, key=critere_glouton)

    # ÉTAPE 2 : TRAITEMENT LINÉAIRE — O(n)
    solution = []
    for element in donnees_triees:
        if condition_faisabilite(solution, element):
            solution.append(element)

    return solution

Conditions de réussite

Un algorithme glouton donne une solution optimale seulement si le problème satisfait deux propriétés fondamentales :

Propriété 1 — Choix glouton (Greedy Choice Property) Il est possible d'atteindre une solution optimale globale en effectuant une séquence de choix localement optimaux, sans avoir besoin de revenir en arrière.

Le choix fait à l'étape actuelle doit être sans danger (safe) et contribuer directement à l'optimalité finale.
Propriété 2 — Sous-structure optimale (Optimal Substructure) Après un choix glouton, le sous-problème restant doit lui aussi contenir une solution optimale au problème original.

Si on effectue le meilleur choix glouton puis qu'on résout le sous-problème restant de façon optimale, la combinaison donne la solution optimale du problème initial.
Propriétés vérifiées ?RésultatAvantagesAlternative si non
✅ Les deuxSolution optimale garantieSimple, efficace, O(n log n), pas de mémoïsation
❌ L'une ou les deuxCorrecte mais non optimaleProgrammation dynamique

Étapes de construction d'un algorithme glouton

Méthodologie en 4 étapes

ÉtapeQuestion cléRendu de monnaieSac à dos fractionnaire
1. Définir le problèmeÉléments ? Contrainte ? But ?Pièces, somme = M, min piècesn objets (vᵢ, wᵢ), poids ≤ C, max valeur
2. Identifier les sous-problèmesQuelle décision à chaque étape ?Quelle pièce choisir ?Quel objet (fraction) ajouter ?
3. Définir la fonction objectifQue minimise/maximise-t-on ?Minimiser ΣxᵢMaximiser Σvᵢ·xᵢ
4. Déterminer le choix gloutonQuel critère local ?Plus grande pièce ≤ restePlus grand ratio vᵢ/wᵢ décroissant

Étape 1 — Définir le problème

Formuler clairement : quels sont les éléments, la contrainte principale, et le but à atteindre ?

 Rendu de monnaie

Formulation
On dispose de pièces de valeurs données (10, 5, 1 DH…).
On veut rendre un montant M en utilisant le minimum de pièces.
  Objectif   : minimiser le nombre de pièces
  Contrainte : somme des pièces = M

 Sac à dos fractionnaire

Formulation
On dispose de n objets, chacun avec une valeur vᵢ et un poids wᵢ.
Le sac a une capacité maximale C. On peut prendre des fractions.
  Objectif   : maximiser la valeur totale
  Contrainte : somme des poids ≤ C

Étape 3 — Définir la fonction objectif

Fonctions objectif — Formulation mathématique Rendu de monnaie : minimiser le nombre total de pièces utilisées : $$\text{Minimiser } f = \sum_{i=1}^{n} x_i \quad \text{sous contrainte} \quad \sum_{i=1}^{n} v_i \cdot x_i = M, \quad x_i \in \mathbb{N},\, x_i \ge 0$$
Sac à dos fractionnaire : maximiser la valeur totale : $$\text{Maximiser } f = \sum_{i=1}^{n} v_i \cdot x_i \quad \text{sous contrainte} \quad \sum_{i=1}^{n} w_i \cdot x_i \le C, \quad 0 \le x_i \le 1$$

Étape 4 — Déterminer le choix glouton

C'est le cœur de l'algorithme : identifier la règle locale qui permet le meilleur choix immédiat. Ce choix doit être simple à évaluer, localement optimal et répété jusqu'à satisfaction des contraintes.

ProblèmeCritère gloutonJustification
Rendu de monnaiePlus grande pièce \(v_i \le\) resteRéduit le reste au maximum à chaque étape
Sac à dos fractionnaireRatio \(r_i = v_i / w_i\) décroissantMaximise la valeur par unité de poids

Cas 1 — Ordonnancement de tâches pondérées

Contexte et formulation L'ordonnancement (scheduling) consiste à organiser un ensemble de tâches à exécuter sur une seule machine, en minimisant un coût global. Ce type de problème apparaît dans la planification industrielle, la gestion de processeurs, la logistique et la gestion de projets.

On dispose de \(n\) tâches \(T = \{T_1, T_2, \dots, T_n\}\), chacune caractérisée par une durée \(d_i > 0\) et un poids \(p_i\) (importance, priorité).

Objectif : minimiser la somme pondérée des temps d'achèvement : $$\text{Minimiser } \sum_{i=1}^{n} p_i \times C_i$$ avec \(C_i\) = durée cumulée jusqu'à la fin de la tâche \(T_i\). Si les poids varient, il faut terminer plus tôt les tâches les plus importantes.
Règle de Smith — Choix glouton Trier les tâches par rapport \(\dfrac{d_i}{p_i}\) croissant : exécuter en premier les tâches à durée faible ou poids fort.

Intuition : une tâche courte et importante doit passer avant les tâches longues et peu prioritaires pour réduire le temps d'attente pondéré des suivantes.

 Exemple — 3 tâches A, B, C

TâcheDurée dᵢPoids pᵢRapport dᵢ/pᵢ
A321.5
B240.5 ← 1er
C414.0

Ordre d'exécution (dᵢ/pᵢ croissant) : B → A → C

OrdreTâcheTemps cumulé Cᵢpᵢ × Cᵢ
1B24 × 2 = 8
2A52 × 5 = 10
3C91 × 9 = 9
Coût total27 ← Optimal

Toute autre permutation donnerait une somme plus grande → la solution gloutonne est optimale.


Python
def ordonnancement_glouton(taches):
    """
    Ordonnancement optimal par la règle de Smith.
    taches : liste de tuples (nom, durée, poids)
    Trie par d_i / p_i croissant → minimise la somme pondérée des Cᵢ.
    """
    taches_triees = sorted(taches, key=lambda T: T[1] / T[2])
    temps, somme = 0, 0

    for nom, d, p in taches_triees:
        temps += d
        somme += p * temps

    return taches_triees, somme


# Exemple
taches = [("A", 3, 2), ("B", 2, 4), ("C", 4, 1)]
ordre, cout = ordonnancement_glouton(taches)
print(f"Ordre : {ordre}")
print(f"Coût total associé = {cout}")
Sortie
Ordre : [('B', 2, 4), ('A', 3, 2), ('C', 4, 1)]
Coût total associé = 27

Cas 2 — Sélection d'activités

Problème de sélection d'activités On dispose d'une ressource unique (salle, machine, employé) et de \(n\) activités, chacune définie par un intervalle \([d_i,\, f_i)\). Deux activités sont compatibles si leurs intervalles ne se chevauchent pas : $$[d_i, f_i) \cap [d_j, f_j) = \emptyset \;\iff\; \lnot\bigl((d_i < f_j) \wedge (d_j < f_i)\bigr)$$ Objectif : sélectionner un sous-ensemble \(S\) de taille maximale, toutes les activités étant deux à deux compatibles : $$\text{Maximiser } |S| = \sum_{i=1}^{n} x_i, \quad x_i \in \{0,1\}$$
Choix glouton — Terminer le plus tôt Parmi toutes les activités encore compatibles, choisir celle qui se termine le plus tôt. Cela libère un maximum de temps pour les activités suivantes.

Algorithme en deux étapes :
  1. Trier les activités par heure de fin \(f_i\) croissante.
  2. Parcourir séquentiellement et sélectionner chaque activité compatible avec la dernière choisie (debut ≥ fin_précédente).

 Exemple — 10 activités

Étape 1 — Tri par fin croissante : A1 (f=4), A2 (5), A3 (6), A4 (7), A5 (9), A6 (9), A7 (10), A8 (11), A9 (12), A10 (14)

Étape 2 — Sélection gloutonne :

ActivitéDébut dᵢFin fᵢSélectionnée ?Raison
A114✅ OuiPremière activité
A235❌ Non3 < 4 (chevauche A1)
A306❌ Non0 < 4 (chevauche A1)
A457✅ Oui5 ≥ 4 (compatible)
A539❌ Non3 < 7 (chevauche A4)
A659❌ Non5 < 7 (chevauche A4)
A7610❌ Non6 < 7 (chevauche A4)
A8811✅ Oui8 ≥ 7 (compatible)
A9812❌ Non8 < 11 (chevauche A8)
A10214❌ NonChevauche tout

Solution gloutonne : {A1, A4, A8} — Nombre maximal d'activités compatibles : 3

Visualisation sur axe temporel
Temps :  0    2    4    6    8   10   12   14
         |    |    |    |    |    |    |    |
A1  :  [=========]
A4  :              [========]
A8  :                        [=========]

← 3 activités, aucun chevauchement ✓

Python
def selection_activites(activites):
    """
    Sélectionne le maximum d'activités compatibles.
    activites : liste de tuples (nom, debut, fin)
    Trie par heure de fin croissante → choisit celle qui se termine le plus tôt.
    """
    activites_triees = sorted(activites, key=lambda x: x[2])
    selection      = []
    fin_precedente = 0

    for nom, debut, fin in activites_triees:
        if debut >= fin_precedente:       # activité compatible
            selection.append((nom, debut, fin))
            fin_precedente = fin          # mettre à jour la dernière fin

    return selection


# Exemple
A = [("A1",1,4),  ("A2",3,5),  ("A3",0,6),  ("A4",5,7),
     ("A5",3,9),  ("A6",5,9),  ("A7",6,10), ("A8",8,11),
     ("A9",8,12), ("A10",2,14)]

res = selection_activites(A)
print("Activités sélectionnées :", res)
Sortie
Activités sélectionnées : [('A1', 1, 4), ('A4', 5, 7), ('A8', 8, 11)]

Récapitulatif

ProblèmeCritère gloutonObjectifOptimal ?
Rendu de monnaie (système standard)Plus grande pièce ≤ resteMin pièces✅ Oui
Rendu de monnaie (système quelconque)Plus grande pièce ≤ resteMin pièces❌ Pas toujours
Ordonnancement pondéréRatio dᵢ/pᵢ croissant (Smith)Min Σpᵢ·Cᵢ✅ Oui
Sélection d'activitésFin fᵢ croissanteMax activités✅ Oui
Sac à dos fractionnaireDensité vᵢ/wᵢ décroissanteMax valeur✅ Oui
Sac à dos entier (0/1)Densité vᵢ/wᵢ décroissanteMax valeur❌ Non → Prog. dyn.
Chemin somme max (arbre)Plus grand fils localMax somme❌ Non → Prog. dyn.
Points clés à retenir
  • Un algorithme glouton fait le meilleur choix local à chaque étape, sans jamais revenir en arrière.
  • Il est optimal si et seulement si le problème vérifie la propriété du choix glouton ET la sous-structure optimale.
  • La complexité est généralement O(n log n) (dominée par le tri initial).
  • Le critère de tri est le cœur du problème : identifier le bon critère garantit l'optimalité.
  • Un contre-exemple suffit à montrer qu'un algorithme glouton n'est pas optimal → recourir alors à la programmation dynamique.
  • Le glouton est plus simple et rapide que la programmation dynamique quand il est applicable : il ne stocke pas de solutions intermédiaires.
Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.