Algorithmes gloutons
Nous allons étudier une grande "famille" d'algorithmes : Les algorithmes gloutons (ou "greedy algorithm" en anglais).
Mais avant de définir les grandes lignes caractérisant cette classe, nous allons nous intéresser à un exemple classique, de la vie courante,
où vous utilisez un algorithme glouton sans le savoir...
Un exemple : Le rendu de monnaie
Présentation du problème
Le rendu de monnaie est un problème du quotidien :
vous devez rendre 4,24€ avec un système de pièces et de billets donnés, en minimisant le nombre de pièce ou billets rendus.
Vous avez déjà résolu ce problème de nombreuses fois, en suivant un algorithme "instinctif".
Nous allons essayer d'analyser votre résolution de ce problème et tenter de généraliser votre démarche à n'importe quelle somme
et à n'importe quel système de monnaie (ensemble de pièces et de billets existants).
Le problème du rendu de monnaie c'est :
- Rendre une somme donnée,
- à partir d'un système de monnaie donné
monnaie=[pièce1, pièce2, ..., billet1, billet2, ...], - en minimisant le nombre de pièces et de billets rendus.
Quelques exemples
Pour bien comprendre la démarche à suivre nous allons commencer par rendre quelques sommes "à la main".
Nous allons considérer notre système de monnaie européen (sans les centimes): monnaie = [1, 2, 5, 10, 20, 50, 100, 200, 500].
Cet exemple est "trivial", nous allons donc nous intéresser à un exemple plus difficile...
Une fois que vous avez traiter ces deux exemples, nous allons nous orienter vers la formalisation de l'algorithme que vous utilisez instinctivement. Pour cela répondez aux questions suivantes :
Implémentation Python
Si vous avez répondu correctement aux questions précédentes, vous avez réussi à cerner l'algorithme que vous appliquez naturellement pour rendre la monnaie. Vous allez maintenant programmer cet algorithme.
Écrire une fonction rendu_glouton répondant aux spécifications suivantes :
- La fonction prendra en paramètres :
- Le système de monnaie sous forme d'un tableau trié dans l'ordre de valeur décroissante ( ça peut être utile ! )
- La somme à rendre
- elle renverra la suite ( sous forme d'un tableau ) des billets et des pièces à rendre
Tester votre fonction sur les appels proposés, que vous avez étudiés ci-dessus ( et dont vous connaissez donc la solution ! )
Les grands principes des algorithmes gloutons
Si votre fonction fonctionne correctement, vous avez réussi à programmer votre premier algorithme glouton; mais quelles sont les grands principes de cette classe d'algorithmes ?
Un algorithme glouton est un algorithme qui fait, à chaque étape de son exécution, un choix local optimal, sans retour en arrière ni anticipation des étapes suivantes, en "espérant" que cela conduise à une solution globale optimale.
Les algorithmes de cette classe n'étudient donc pas systématiquement toutes les solutions possibles afin d'en déduire la meilleure ( algorithmes dits de "force brute"), mais ils sont beaucoup plus rapides.
Dans l'exemple précédent, nous avions à chaque étape :
- choisi le plus gros billet/la plus grosse pièce = choix local optimal : plus le billet/la pièce est gros(se), moins on en rendra,
- en espérant rendre ainsi le moins de billets possibles = solution globale optimale.
Et dans notre cas, CA MARCHE !!!.
Mais...les algorithmes gloutons ne "fonctionnent" pas toujours :
- Le rendu de monnaie n'est pas forcément optimal
- le rendu de monnaie peut ne pas aboutir
[4,3,1], en suivant votre algorithme. Est-ce optimal ? Justifier.
2. Rendez 8€ avec le système suivant [10,5,2]; mêmes questions ?
Pour information, notre système de monnaie européen a été mis au point pour que l'algorithme de rendu de monnaie glouton soit toujours optimal !
Le problème du sac à dos
Présentation du problème
Le problème du sac à dos est un problème d'optimisation classique de l'informatique que nous allons tenter de résoudre grâce aux algorithmes gloutons.
Nous allons imaginer un voleur en train de dévaliser un appartement.
- Il ne peut pas porter un sac de plus de 30 kg,
- il veut ramener un butin maximum,
et il a devant lui plusieurs objets :
- objet 1 : poids 13 kg et valeur 700 €
- objet 2 : poids 12 kg et valeur 400 €
- objet 3 : poids 8 kg et valeur 300 €
- objet 4 : poids 10 kg et valeur 300 €
Il doit donc optimiser le remplissage de son sac.
Un peu de force brute (à la main !)
La technique de "force brute" consiste à attaquer un problème frontalement et à explorer toutes les possibilités en profitant de la force de calcul d'un ordinateur.
C'est une technique qui peut, par exemple, être utilisée pour trouver un mot de passe inconnu, en testant toutes les possibilités.
En considérant la liste d'objets ci-dessus, répondre aux questions suivantes:
On peut remarquer qu'avec 4 objets, le problème est déjà assez complexe !
Mais dès que l'on dépasse 40 objets, un ordinateur actuel est incapable de le résoudre raisonnablement de cette façon !
En effet la complexité de ce problème en en O(2n), où n est le nombre d'objets.
Et l'algorithme glouton ?
Comme on vient de le voir, il n'est pas envisageable de tester tous les cas, surtout si le nombre d'objets dépasse 30 ou 40.
Nous allons donc tenter d'appliquer la technique de l'algorithme glouton à ce problème.
Mais comment faire ?
Faut-il choisir initialement le plus petit objet, ou celui qui rapporte le plus ?
Et bien il faut OPTIMISER.
Nous allons donc calculer pour chaque objet sa "valeur massique" c'est à dire ce que rapportera un kg de cet objet :
- objet 1 : poids 13 kg et valeur 700 €, valeur massique : 700/13 = 53,8 €/kg
- objet 2 : poids 12 kg et valeur 400 €, valeur massique : 400/12 = 33,3 €/kg
- etc...
- on trie alors cette liste d'objets et on commence par celui qui rapporte le plus "au kg";
- on vérifie que le sac n'est pas trop lourd si on l'ajoute, sinon on passe au suivant,
- on enlève l'objet sélectionné de la liste des objets,
- ...et on recommence avec les objets suivants !
Structure de données utilisée
Pour résoudre ce problème il faut d'abord penser à la structure de données qui pourra contenir la liste d'objets.
Sachant que chaque objet a 4 caractéristiques :
- Son nom,
- Sa masse,
- Sa valeur,
- Sa valeur massique.
Il va donc être judicieux d'utiliser pour caractériser chaque objet un dictionnaire {nom, masse, valeur, gain_massique}; l'ensemble de ces dictionnaires sera alors
stocké dans un tableau liste_objets :
liste_objets = [
{"nom": "objet1", "masse": 13, "valeur": 700, "val_mas": None}, # pour l'instant, la valeur massique n'a pas encore été calculée
{"nom": "objet2", "masse": 12, "valeur": 400, "val_mas": None},
{"nom": "objet3", "masse": 8, "valeur": 300, "val_mas": None},
{"nom": "objet4", "masse": 10, "valeur": 300, "val_mas": None}
]
Écrire une fonction calcule_gain, qui prend comme paramètre la liste des objets, et qui renvoie cette liste complétée avec la valeur massique
calculée de chaque objet.
Tri de la liste d'objets
Nous allons ici utiliser la fonction sort de Python, que vous avez déjà utilisée pour trier des tables de données.
- Écrire une fonction
cle_tri, qui sera utilisée comme critère de tri pour le tableau liste_objets. - utiliser cette fonction avec la fonction Python
sortpour trier la liste des objets par valeur massique décroissante.
La fonction sac_a_dos
Voici ci-dessous un code incomplet, complétez le et testez le sur la liste d'objets données en introduction :
Test de la fonction avec une liste d'objets plus conséquente...
C'est bien gentil, une liste de 4 objets...mais on aimerait savoir si l'algorithme résoudrait aussi, le problème avec une liste de 40 objets par exemple sans y passer la vie entière !
La fonction ci-dessous renvoie une liste aléatoire de n objets; utiliser cette fonction pour tester la robustesse de votre fonction sac_a_dos :
Ne pas donner une valeur trop grande à n...
Analyse de l'efficacité de l'algorithme glouton
Mais au fait, est-ce que notre algorithme glouton nous propose toujours "LA" solution optimale ?
Dans le cas de la liste liste_objets que vous avez utilisé pendant cette partie, cela semble être le cas.
On considère deux objets, l'un de 1 kg et l'autre de 30 kg.
Comme on l'a déjà dit, la "force brute" ne permet pas de trouver une solution quand il y a trop d'objets.
L'algorithme glouton lui proposera toujours rapidement une solution; elle ne sera "pas trop mauvaise" mais ne sera pas toujours optimale...
Il existe des solutions algorithmiques optimales à ces problèmes, qui peuvent faire appel, par exemple
à la programmation dynamique vue en classe Terminale.
Application : un planning de films...
C'est la fête du cinéma ! Les salles de votre ville ont programmé une série de films qui seront projetés pendant 8h d'affilée.
Une quinzaine de films sont à l'affiche, qui n'ont pas la même heure de projection, ni la même durée; voila le planning prévu de projection :
Le problème qui se pose à vous : bien sûr, vous ne pouvez voir qu'un seul film à la fois, mais vous aimeriez voir le maximum de films, tous vous intéressent
autant ( surtout le film ouzbek ).
Il va donc falloir faire des choix sur la liste des films à voir : quelle est la meilleure stratégie à adopter pour cela ?
Principe
La contrainte pour que deux films soient "compatibles", est que leurs séances ne se "chevauchent" pas : par exemple, C8 et C10 sont compatibles, mais C13 et C15 ne le sont pas.
L'heure de début d'un film doit être supérieure ( ou égale, on suppose que vous pouvez passer instantanément d'une salle à une autre... ) à l'heure de fin du précédent.
Là aussi, l'étude systématique de toutes les combinaisons de films est impossible, vous allez donc adopter une stratégie gloutonne pour faire votre sélection :
- classer les films selon un critère donné ( voir ci-dessous )
- choisir le premier film dans ce classement
- dans la liste des films suivants dans le classement :
- si un film est compatible avec le précédent, alors on l'ajoute à la liste des films à voir,
- sinon, on passe au film suivant.
La question est donc celle du critère à utiliser pour classer les films, et faire à chaque étape le meilleur choix pour le résultat global ( à savoir, voir le maximum de films ).
Plusieurs stratégies sont possibles pour classer les films :
- par durée ( la plus courte/la plus longue d'abord )
- par heure de début ( début au plus tôt/au plus tard )
- par heure de fin ( fin au plus tôt/au plus tard )
...soit donc 6 possibilités, ça semble compliqué....heureusement, l'informatique va vous permette de résoudre votre problème !
Structure de données utilisée
Le séances seront représentées par un tuple (film, début, fin); un tableau seances contiendra l'ensemble des tuples représentant les séances.
Vous trouverez ci-dessous une fonction affiche_seances, qui permet de visualiser un ensemble de séances :
Tri des séances
Écrire 3 fonctions debut, fin, duree, qui serviront à trier, à l'aide de la méthode sort de Python, le tableau des séances selon les différents critères.
Tester les fonctions pour trier le tableau seances, sans oublier de tester les ordres ascendant et descendant.
Implémentation de l'algorithme
Compléter le code de la fonction choix_seances ci-dessous, qui prend comme paramètres le tableau des séances trié selon un des critères ci-dessus, et qui renvoie le tableau des séances
compatibles pouvant être suivies, sélectionnées grâce à l'algorithme glouton ci-dessus.
Utiliser la fonction avec les différents critères de tri possible, et visualiser le résultat : quelles stratégies sont manifestement à éliminer ?
La meilleure stratégie
Pour départager les stratégies à adopter et trouver la meilleure, vous allez utiliser votre fonction avec une liste de séances beaucoup plus conséquente.
La fonction ci-dessous génère une liste de n séances d'heure et de début aléatoire, sur une plage de 24h.
Utiliser cette fonction pour déterminer quelle est la meilleure stratégie à adopter afin de voir le maximum de films !
On montre que, en plus d'être la meilleure stratégie gloutonne, ce mode de sélection conduit à la solution optimale du planning des séances : cet algorithme est donc vraiment utilisé pour faire des plannings.
Dans le même ordre d'idée, les algorithmes gloutons se montrent efficaces pour :
- déterminer le plus court chemin sur une carte ou dans un réseau
- compresser des données
- organiser au mieux le parcours d'un voyageur visitant un ensemble de ville
- organiser au mieux des plannings d'activité ou d'occupations de salles