Nous allons étudier une grande "classe" 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 gloutons sans le savoir.
Un exemple : Le rendu de monnaie
Présentation du problème
Le rendu de monnaie est un problème du quotidien. la question est la suivante :
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
A 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): [1, 2, 5, 10, 20, 50, 100, 200, 500].
Trouver TOUTES les possibilités pour rendre 7€ (il y en a 6 différentes ).
Cette exemple est "trivial", nous allons donc nous intéresser à un exemple plus difficile...
Nous allons tenter de rendre 263 €, je ne vous demande pas de trouver toutes les solutions, puis de déterminer celle qui est optimale mais de donner, d'après vous, la solution optimale.
Une fois que vous avez réalisé ces deux exemples, nous allons nous orienter vers la formalisation de l'algorithme que vous utilisez instinctivement. Pour cela répondez aux questions suivantes :
Quel est le premier billet à rendre dans les deux cas précédents ?
Quels sont les deux critères pour choisir ce premier billet ?
Une fois que ce billet est choisit, quel est le nouveau problème qui se pose à vous ?
Si vous avez répondu correctement aux questions précédentes, vous avez réussi à cerner algorithme que vous appliquez naturellement pour rendre la monnaie. Nous allons tenter de programmer cet algorithme.
Vous allez devoir coder une fonction répondant aux caractéristiques suivantes :
La fonction prendra en paramètre :
Le système de monnaie sous forme d'un tableau
La somme à rendre
son entête sera donc def rendu_glouton(monnaie:list, somme:int):
La fonction affichera, dans un premier temps, la suite de billets et de pièces à rendre
L'algorithme à suivre sera donc le suivant, d'après vos recherches précédentes :
tant que somme est positif
si le premier billet est supérieur à somme
passer au billet suivant
sinon:
afficher le billet
enlever le billet à somme
Vous pourrez tester votre fonction sur les appels suivants, que vous avez déjà rencontré :
Si votre fonction fonctionne vous avez réussi à programmer votre premier algorithme glouton, mais quelles sont les grand principes de cette classe d'algorithme ?
Un algorithme glouton est un algorithme qui fait un choix local
optimal en espérant que cela conduise à une solution globale
optimale.
Dans l'exemple précédent nous avons à chaque instant:
Choisi le plus gros billet (choix local optimal, plus le billet est gros, 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
Tentez de rendre 6€ avec le système suivant : [4,3,1], en suivant votre algorithme. Est-ce optimal ?
Tentez de rendre 8€ avec le système suivant [10,5,2]
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 !
Bonus : Pour aller plus plus loin
Vous devez ici améliorer votre fonction def rendu_glouton(monnaie:list, somme:int): en def rendu_glouton(monnaie:list, somme:int)->list:.
La fonction ne doit plus afficher les pièces et billets à rendre mais renvoyer un tableau contenant les pièces et les billets.
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.
Présentation du problème
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 une 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 !)
Vous connaissez sûrement cette expression informatique de "force brute". Elle 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:
Lister les chargements possibles du sac à dos sans se soucier du prix ou de la masse totale pour
4 objets (1 solution c'est facile !)
3 objets (4 solutions)
2 objets (6 solutions)
1 objet (4 solutions c'est facile !)
En vous mettant par groupe de deux, calculer pour chaque chargement :
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.
Calculer 240
Si une opération sur l'ordinateur prend 1µs=10-6s, combien de temps prendront 240 opérations ?
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 son "gain massique" c'est à dire ce que rapportera un kg de cet objet. Ainsi la liste précédente sera agrémentée du gain massique :
objet 1 : poids 13 kg et valeur 700 €, gain massique : 700/13=53,8 €/kg
objet 2 : poids 12 kg et valeur 400 €, gain massique : 400/12=33,3 €/kg
objet 3 : poids 8 kg et valeur 300 €, gain massique : 300/8=37,5 €/kg
objet 4 : poids 10 kg et valeur 300 €, gain massique : 300/10=30 €/kg
Il ne reste plus qu'a trier cette liste d'objets et à commencer par celui qui rapporte le plus "au kg"...
Vérifier que le sac n'est pas trop lourd si on l'ajoute
Enlever cet objet de la liste
...et recommencer !
Structure de données
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 trois caractéristiques :
Sa masse
Sa valeur
Sa valeur massique
Il va donc être judicieux d'utiliser un tableau de tableau:
il va falloir trier la liste d'objets en utilisant le critère de la valeur massique.
A priori la valeur massique n'est pas donnée au départ, cela sera à notre programme de la calculer
Nous verrons un peu plus tard dans l'année différents algorithmes de tri, nous allons donc utiliser la méthode sort() de python.
Testez le programme ci-dessous :
Vous avez compris la syntaxe, mais nous voulons trier un tableau de tableaux en choisissant un élément particulier comme critère de prix (la valeur massique).
Testez le code ci-dessous :
None représentant ici l'absence de valeurs, la valeur massique de chaque objet sera calculée dans la fonction sacAdos.
Grâce au tableau de tableaux obj_test nous allons pouvoir tester notre futur programme. Mais si l'on veut voir s'il "résiste" à une liste de 30 objets, il faudra générer automatiquement une telle liste.
Le code ci dessous est incomplet. Vous devez le compléter pour qu'il respecte la docstring.
from random import randint
def listeobj(n:int)->list:
'''
n est le nombre d'objets dans la liste
la fonction renvoie une liste d'objets aléatoire pour le probleme du sac a dos
'''
obj=[[None,randint(1,30), randint(10,100)]...]
....
vous pourrez tester ce code avec les deux appels suivants :
Voici un code incomplet, complétez le et testez le en suivant les questions ci-dessous.
def sacAdos(objets:list,msac:int)-> list:
'''
résolution du problème du sac à dos
objets est la liste d'objets
msac est la masse maximale du sac
la fonction renvoie la somme présente dans le sac et la masse du sac
'''
masse=0
somme=0
#parcours de la liste d'objet et calcul de la valeur massique
for i in range(len(objets)):
objets[i][0]=...
#tri de la liste d'objets du plus rentable au moins rentable
objets.sort(reverse=True)
#parcours de la liste d'objets et choix de ceux à mettre dans le sac
for element in objets:
if masse+element[1] < msac:
...
...
return ...
Dans la première partie du programme, on calcule la valeur massique de chaque objet que l'on range à la case "0" du sous-tableau représentant chaque objet. Compléter les pointillés de la ligne 13 pour réaliser cette fonction.
Tester grâce à un affichage cette partie du programme sur le tableau de tableau obj_test.
Dans la deuxième partie du programme, on parcourt chaque élément de la liste et on teste si il est possible de l'ajouter dans le sac à dos (lignes 19 et 20). Si cela est possible, deux opérations sont à réaliser avant de passer à l'objet suivant. Compléter les lignes 21 et 22.
Compléter enfin la ligne 24 en respectant la docstring.
Pour finir testez votre programme sur obj_test et sur une liste de 30 objets aléatoires
Mais au fait, est-ce que notre algorithme glouton nous propose "LA" solution optimale ?
Dans le cas de la liste "test" que vous avez utilisé pendant toute cette partie cela semble être le cas.
Si l'on considère deux objets, l'un de 1kg et l'autre de 30kg.
Attribuez à chacun une valeur qui fera que notre algorithme glouton ne sera pas optimal.
Comme on l'a déjà la "force brute" ne permet pas de trouver une solution quand il y a trop d'objets.
L'algorithme glouton lui proposera toujours une solution, elle sera "pas trop mauvaise" mais ne sera pas toujours optimale...
Il existe des solutions algorithmiques optimales à ce problème, mais elle sont totalement hors programme !
Elle peuvent faire appel, par exemple à la programmation dynamique.