Adloun

Algorithmes gloutons

Cours complet · OCaml (option informatique), chapitre 12 · prépas MPSI et MP, option informatique

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

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

Le chapitre précédent explorait tout l'espace des choix par retour sur trace — exact, mais souvent exponentiel. À l'opposé, un algorithme glouton ne se pose qu'une question à chaque étape : « quel est le meilleur choix local, ici et maintenant ? » ; il le fait, et ne revient jamais dessus. C'est rapide (un seul parcours, sans exploration), mais ce n'est pas toujours optimal : tout l'art est de reconnaître les problèmes où la stratégie gloutonne donne, malgré sa myopie, la solution optimale.

Ce chapitre présente le principe glouton sur deux problèmes classiques — le rendu de monnaie et la sélection d'activités — puis montre, par un contre-exemple, où le glouton se trompe, ce qui motivera la programmation dynamique du chapitre suivant.

12.2 Le principe glouton

Méthode : Stratégie gloutonne

Pour construire une solution pas à pas :

  • à chaque étape, faire le choix qui semble le meilleur sur le moment (le plus grand, le plus court, le moins cher…) ;
  • ne jamais remettre ce choix en question.

C'est glouton car on saisit le gain immédiat sans anticiper. Avantage : très rapide (pas de retour arrière). Risque : un bon choix local peut interdire la meilleure solution globale.

12.3 Le rendu de monnaie

Rendre un montant avec le moins de pièces possible : la stratégie gloutonne prend, à chaque étape, la plus grande pièce qui ne dépasse pas ce qui reste à rendre.


(* pieces : valeurs en ordre DÉCROISSANT, en quantité illimitée *)
let rec rendu pieces montant =
  match pieces with
  | [] -> if montant = 0 then [] else failwith "rendu impossible"
  | p :: reste ->
      if p <= montant then p :: rendu pieces (montant - p)
      else rendu reste montant

Tant que la plus grande pièce p convient, on la réutilise (la liste pieces ne change pas) ; sinon on passe à la suivante. rendu [50; 20; 10; 5; 2; 1] 88 renvoie [50; 20; 10; 5; 2; 1].

ImportantLe glouton dépend du système de pièces

Sur le système euro [50; 20; 10; 5; 2; 1], le glouton donne toujours le nombre minimal de pièces : on dit que le système est canonique. Mais ce n'est pas une loi générale ! Avec [4; 3; 1] et un montant de 6, le glouton prend 4 puis 1 + 1 (trois pièces), alors que 3 + 3 n'en demande que deux. Le glouton se trompe : il faut un autre algorithme (programmation dynamique) pour l'optimum sur un système quelconque.

12.4 La sélection d'activités

On dispose d'activités, chacune avec un début et une fin, et l'on veut en retenir le plus grand nombre qui ne se chevauchent pas (une seule salle). La bonne stratégie gloutonne : trier par heure de fin croissante, et prendre chaque activité compatible avec la dernière retenue.


type activite = { debut : int; fin : int }

(* tri par fin croissante (insertion) *)
let rec tri_fin l =
  let rec insere a l =
    match l with
    | [] -> [a]
    | b :: reste -> if a.fin <= b.fin then a :: l else b :: insere a reste
  in
  match l with
  | [] -> []
  | a :: reste -> insere a (tri_fin reste)

let selection activites =
  match tri_fin activites with
  | [] -> []
  | premiere :: reste ->
      let rec choisir fin_courante l =
        match l with
        | [] -> []
        | a :: r ->
            if a.debut >= fin_courante then a :: choisir a.fin r
            else choisir fin_courante r
      in
      premiere :: choisir premiere.fin reste

On retient toujours l'activité qui finit le plus tôt (la première après tri), puis on saute celles qui la chevauchent et l'on recommence.

iRemarquePourquoi finir tôt est optimal

Choisir l'activité qui se termine le plus tôt laisse le maximum de place pour les suivantes. Un argument d'échange le prouve : dans n'importe quelle solution optimale, on peut remplacer la première activité par celle qui finit le plus tôt sans réduire le nombre total — donc une solution optimale contenant ce choix glouton existe. C'est ce type de preuve qui justifie qu'un glouton est correct.

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

Niveau (application directe du cours)

Exercice 1 : Rendu de monnaie glouton

Écrire rendu et donner rendu [50; 20; 10; 5; 2; 1] 67.

Démonstration

let rec rendu pieces montant =
  match pieces with
  | [] -> if montant = 0 then [] else failwith "rendu impossible"
  | p :: reste ->
      if p <= montant then p :: rendu pieces (montant - p)
      else rendu reste montant

rendu [50;20;10;5;2;1] 67 [50; 10; 5; 2], soit quatre pièces ().

Exercice 2 : Nombre de pièces

Écrire nb_pieces pieces montant (sans construire la liste).

Démonstration

let rec nb_pieces pieces montant =
  match pieces with
  | [] -> if montant = 0 then 0 else failwith "rendu impossible"
  | p :: reste ->
      if p <= montant then 1 + nb_pieces pieces (montant - p)
      else nb_pieces reste montant

Même structure que rendu, mais on compte au lieu d'accumuler. nb_pieces [50;20;10;5;2;1] 67 .

Exercice 3 : Compatibilité de deux activités

Écrire compatibles a b : deux activités peuvent-elles coexister (pas de chevauchement) ?

Démonstration

let compatibles a b = a.fin <= b.debut || b.fin <= a.debut

Deux activités sont compatibles si l'une finit avant que l'autre commence (dans un sens ou dans l'autre). On adopte la convention « fin début » : une activité qui finit à l'instant où une autre commence ne la chevauche pas.

Niveau (raisonnement intermédiaire)

Exercice 4 : Trier par heure de fin

Écrire tri_fin qui trie une liste d'activités par fin croissante.

Démonstration

let rec tri_fin l =
  let rec insere a l =
    match l with
    | [] -> [a]
    | b :: reste -> if a.fin <= b.fin then a :: l else b :: insere a reste
  in
  match l with
  | [] -> []
  | a :: reste -> insere a (tri_fin reste)

C'est le tri par insertion du chapitre 9, mais comparant le champ fin des enregistrements (un tri générique &lt; les comparerait par debut d'abord, ce qu'on ne veut pas). Trier par fin est l'étape clé du glouton.

Exercice 5 : Sélection d'activités

Écrire selection qui renvoie la liste des activités retenues.

Démonstration

let selection activites =
  match tri_fin activites with
  | [] -> []
  | premiere :: reste ->
      let rec choisir fin_courante l =
        match l with
        | [] -> []
        | a :: r ->
            if a.debut >= fin_courante then a :: choisir a.fin r
            else choisir fin_courante r
      in
      premiere :: choisir premiere.fin reste

On retient la première (celle qui finit le plus tôt), puis on parcourt les suivantes : chaque activité dont le debut est à la fin_courante est retenue (et devient la nouvelle référence) ; les autres sont écartées.

Exercice 6 : Nombre maximal d'activités

Écrire nombre_max activites (combien d'activités au plus sans chevauchement).

Démonstration

let rec longueur l =
  match l with [] -> 0 | _ :: r -> 1 + longueur r

let nombre_max activites = longueur (selection activites)

On réutilise selection et l'on compte. Le glouton « fin la plus tôt » garantit que ce nombre est maximal (cf. l'argument d'échange).

Niveau (approfondissement)

Exercice 7 : Rendu optimal exhaustif

Écrire rendu_min pieces montant qui calcule le nombre minimal de pièces (système quelconque, pièces illimitées), par exploration récursive.

Démonstration

let rec rendu_min pieces montant =
  if montant = 0 then 0
  else begin
    let infini = montant + 1 in     (* borne : jamais plus de 'montant' pièces (pièce 1) *)
    let meilleur = ref infini in
    let rec essaie l =
      match l with
      | [] -> ()
      | p :: reste ->
          if p <= montant then begin
            let r = rendu_min pieces (montant - p) in
            if 1 + r < !meilleur then meilleur := 1 + r
          end;
          essaie reste
    in
    essaie pieces;
    !meilleur
  end

On essaie chaque pièce comme premier choix, et l'on garde le meilleur sous-rendu : c'est l'optimum, mais au prix d'une explosion (le même montant est recalculé maintes fois — la mémoïsation du chapitre 8, ou la programmation dynamique du chapitre suivant, y remédient).

Exercice 8 : Quand le glouton se trompe

Comparer nb_pieces (glouton) et rendu_min (optimal) sur le système [4; 3; 1] et le montant 6, et expliquer.

Démonstration

nb_pieces [4;3;1] 6 : le glouton prend 4 (reste 2), puis 1 + 1 → 3 pièces. rendu_min [4;3;1] 6 : le minimum est 3 + 3 → 2 pièces. Le glouton échoue car son premier choix (la grosse pièce 4) bloque la meilleure combinaison. Sur un système non canonique, seule une méthode exhaustive (ou la programmation dynamique) garantit l'optimum. C'est la limite fondamentale du glouton.

Exercice 9 : Sac à dos fractionnaire

On peut prendre des fractions d'objets. Écrire sac_fractionnaire objets capacite maximisant la valeur emportée, par glouton.

Démonstration

type objet = { valeur : float; poids : float }

let rec tri_ratio l =          (* tri décroissant par valeur/poids *)
  let rec insere o l =
    match l with
    | [] -> [o]
    | x :: reste ->
        if o.valeur /. o.poids >= x.valeur /. x.poids then o :: l
        else x :: insere o reste
  in
  match l with
  | [] -> [] | o :: reste -> insere o (tri_ratio reste)

let sac_fractionnaire objets capacite =
  let rec prendre l reste =
    match l with
    | [] -> 0.0
    | o :: suite ->
        if o.poids <= reste then o.valeur +. prendre suite (reste -. o.poids)
        else o.valeur *. (reste /. o.poids)   (* fraction du dernier objet *)
  in
  prendre (tri_ratio objets) capacite

On trie par rapport valeur/poids décroissant et on remplit : objets entiers tant qu'ils tiennent, puis une fraction du suivant. Le glouton est ici optimal (on peut couper). Attention : pour le sac à dos « 0/1 » (objets indivisibles), ce glouton n'est plus optimal — il faut la programmation dynamique.

Exercice 10 : Nombre minimal de salles

Écrire salles activites : le nombre minimal de salles pour héberger toutes les activités (deux activités qui se chevauchent exigent deux salles).

Démonstration

Le nombre minimal de salles égale le nombre maximal d'activités simultanées. On trie séparément les débuts et les fins (tri_fusion du chapitre 9 sur des entiers) et l'on balaie le temps :


let salles activites =
  let debuts = tri_fusion (List.map (fun a -> a.debut) activites) in
  let fins = tri_fusion (List.map (fun a -> a.fin) activites) in
  let rec balayage debuts fins courant maxi =
    match debuts, fins with
    | [], _ -> maxi
    | d :: rd, f :: rf ->
        if d < f then
          let c = courant + 1 in
          balayage rd fins c (if c > maxi then c else maxi)
        else
          balayage debuts rf (courant - 1) maxi
    | _ -> maxi
  in
  balayage debuts fins 0 0

On avance dans le temps : un début avant la prochaine fin fait monter le nombre d'activités en cours (courant) — on note le pic maxi ; une fin le fait baisser. Le pic est le nombre de salles nécessaires. (d &lt; f : une activité qui finit pile quand une autre commence libère la salle à temps.)

Synthèse du chapitre (à retenir)
  • Glouton : à chaque étape, le meilleur choix local, sans retour arrière. Rapide, mais pas toujours optimal.
  • Rendu de monnaie : prendre la plus grande pièce possible. Optimal sur un système canonique (euro), pas sur un système quelconque (contre-exemple [4;3;1], montant 6 : glouton , optimal ).
  • Sélection d'activités : trier par fin croissante, prendre chaque compatible. Optimal, justifié par un argument d'échange (finir tôt laisse le plus de place).
  • Sac à dos fractionnaire : trier par rapport valeur/poids, remplir ; glouton optimal. Mais le sac « 0/1 » ne l'est pas.
  • Prouver qu'un glouton est correct demande un argument (échange, structure du problème) ; sinon, recourir au backtracking (chapitre 11) ou à la programmation dynamique (chapitre suivant).

12.6 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Rendu de monnaie.
Thème B — Activités et intervalles.
Thème C — Autres gloutons.
Thème D — Glouton ou pas glouton.

Continuer sur Adloun : animation, QCM, fiches, exercices