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].
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.
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)
É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 ().
É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 .
É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)
É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 < les comparerait par debut d'abord, ce qu'on ne veut pas). Trier par fin est l'étape clé du glouton.
É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.
É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)
É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).
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.
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.
É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 < f : une activité qui finit pile quand une autre commence libère la salle à temps.)
- 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], montant6: 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.
- [11.] Tester
rendusur[10; 5; 2]et le montant8; que se passe-t-il, et pourquoi ? - [12.] Écrire
rendu_possible pieces montant : bool(peut-on rendre exactement le montant ?). - [13.] Donner un autre système de pièces où le glouton échoue, et le montant le plus petit qui le met en défaut.
Thème B — Activités et intervalles.
- [14.] Modifier
selectionpour maximiser non pas le nombre d'activités mais la durée totale ; le glouton « fin la plus tôt » reste-t-il optimal ? - [15.] Écrire
couverture points: le nombre minimal d'intervalles unités pour couvrir une liste de points entiers (glouton). - [16.] Vérifier expérimentalement l'optimalité de
selectionen la comparant à une recherche exhaustive (chapitre 11) sur de petites instances.
Thème C — Autres gloutons.
- [17.]
plus_grand_nombre l: à partir d'une liste de chiffres, former le plus grand entier (trier les chiffres décroissants). - [18.]
sac_01_glouton: appliquer le glouton par ratio au sac à dos indivisible, et exhiber un cas où il n'est pas optimal. - [19.] Ordonnancement : tâches de durées données sur une machine ; ordonner pour minimiser le temps d'attente total (glouton : plus courte d'abord). Justifier.
Thème D — Glouton ou pas glouton.
- [20.] Pour chacun : rendu euro, sélection d'activités, sac 0/1, plus court chemin — dire si un glouton simple est optimal.
- [21.] Implémenter
rendu_minmémoïsé (table de hachage du chapitre 8) et comparer les temps avec la version exhaustive. - [22.] Discuter : quelles propriétés un problème doit-il avoir pour qu'un glouton soit optimal (sous-structure optimale, propriété du choix glouton) ?