Métaheuristiques et algorithmes probabilistes
Métaheuristiques et algorithmes probabilistes
Annexe au chapitre 9
Métaheuristiques
A9.1. Quelques définitions
• En optimisation combinatoire, théorie des graphes et théorie de la complexité, une
heuristique est un algorithme qui fournit rapidement (en temps polynomial) une solution
réalisable, mais pas nécessairement optimale, pour un problème d'optimisation difficile.
Une heuristique, ou méthode approximative, est donc le contraire d'un algorithme exact qui
trouve une solution optimale pour un problème donné. L'usage d'une heuristique est
pertinente pour calculer une solution approchée d'un problème et ainsi accélérer le
processus de résolution exacte.
• Les métaheuristiques forment une famille d'algorithmes d'optimisation visant à résoudre des
problèmes d'optimisation difficile (souvent issus des domaines de la recherche
opérationnelle, de l'ingénierie ou de l'intelligence artificielle) pour lesquels on ne connaît
pas de méthode classique plus efficace. Ces méthodes utilisent cependant un haut niveau
d'abstraction, leur permettant d'être adaptées à une large gamme de problèmes différents.
Les métaheuristiques les plus connues sont la recherche avec tabous, le recuit simulé, les
algorithmes génétiques et les colonies de fourmis.
Exercice A9.1
Développez et programmez une méthode basée sur l'estimation de l'aire d'un quart de cercle pour
approcher la valeur de p.
Exercice A9.2
Modifiez le programme de l'exercice 9.7 qui teste si un point est à l'intérieur d'un polygone, afin
d'estimer l'aire de ce polygone. Pour ce faire, vous générerez 100'000 points au hasard et calculerez
le pourcentage de points « tombés » à l'intérieur du polygone.
On répétera cet algorithme des milliers de fois et on n'affichera que la meilleure solution trouvée.
Programmez cet algorithme en Python.
Exercice A9.4
A9.4
Takeshi Kitano est un acteur et un réalisateur japonais né en 1947.
Lors de l'exposition « Mathématiques, un dépaysement soudain » en 2011, il proposa un défi
mathématique aux visiteurs : trouver la formule la plus courte permettant de calculer 2011 en
écrivant, dans l'ordre, les premiers nombres entiers, séparés par des opérateurs (+, -, *, /, exposants,
factorielle).
Exercice A9.5
A9.5
Il existe un algorithme permettant de trouver une solution quel que soit n supérieur à 3 :
1. soit r = n mod 12
2. écrire les nombres pairs de 2 à n
3. si r = 3 ou r = 9 mettre le 2 à la fin de la liste
4. écrire ensuite les nombres impairs de 1 à n, mais si r=8 permuter les nombres
impairs 2 par 2 (i.e. 3, 1, 7, 5, 11, 9, ...)
5. si r = 2, permuter les places de 1 et 3, puis mettre 5 à la fin de la liste
6. si r = 3 ou r = 9, mettre 1 puis 3 en fin de liste
Programmez cet algorithme en Python. Il donne une solution au problème des n dames. Ainsi,
pour n = 8 on obtient la position [2, 4, 6, 8, 3, 1, 7, 5] (dans cet algorithme, les lignes sont numérotées
à partir de 1). Pour n = 15, on aura la solution [4, 6, 8, 10, 12, 14, 2, 5, 7, 9, 11, 13, 15, 1, 3].
Vérifiez la validité de cet algorithme pour n = 4 ... 1000.
Méthode taboue
Rappelons que l'on
passe d'une position
1. Générer une position de départ.
à une position
2. Parmi les positions voisines, choisir la meilleure qui n'est pas dans la
voisine en
liste des tabous.
échangeant deux
3. Si la liste des tabous est pleine, retirer de cette liste la position la plus
colonnes.
ancienne.
4. Mettre la position actuelle en queue de la liste des tabous.
5. Retourner à 2, tant qu'on n'a pas décidé de s'arrêter.
Les positions déjà explorées sont conservées dans une file (voir § 6.2), souvent appelée liste des
tabous, d'une taille donnée. Cette file doit conserver des positions complètes, mais cela ne pose pas
de problèmes avec les n dames. Pour nos expériences, nous avons utilisé une liste de 10 mouvements
tabous.
Résultats avec la méthode taboue
Nombre de dames (n) 20 40 60 80 100
Temps ou nombre de conflits restants 1 sec. 15 sec. 114 sec. 422 sec. 1756 sec.
Commentaires
• Le programme ne se bloque plus dans un minimum local.
• La solution finale est toujours la même et dépend de la position initiale.
• Avec 100 dames, on passe d'une situation avec 4950 conflits à une configuration sans
conflit en 95 échanges de deux colonnes.
Recuit simulé
1. Choisir une « température » de départ T.
Remarque sur le 2. Générer une position aléatoire. Appelons-la P1.
point 4 3. Copier la position P1 dans une position P2, puis échanger deux colonnes de P1
choisies aléatoirement.
Ici, on cherche à
4. Calculer D = score(P2) - score(P1)
minimiser le score,
qui est le nombre 5. Si D 0, P1 P2 ; le cas échéant, mettre à jour le meilleur score et la
meilleure position. Aller à 8.
Algorithme génétique
1. Générer une population de 2n positions aléatoires.
2. Classer et numéroter ces 2n positions selon leur score, du meilleur au moins
bon.
3. Croiser les grilles 2k-1 et 2k, pour k allant de 1 à n.
4. Effectuer une mutation pour chaque position : avec une certaine probabilité,
effacer une case et placer un nouveau chiffre de 0 à 7.
5. Classer et numéroter les 2n grilles obtenues selon leur score, du meilleur au
moins bon.
6. Dupliquer la grille 1 (la meilleure) et placer ce doublon en position 2n,
après avoir éliminé la grille 2n (la moins bonne).
7. Le cas échéant, mettre à jour le meilleur score et la meilleure position.
8. Retourner à 3, tant qu'on n'a pas décidé de s'arrêter.
• Chaque position est représentée pas une liste de n nombres indiquant la ligne occupée pour
la colonne correspondante.
Avec 8 dames, la position ci-dessous est représentée par la liste [0, 5, 1, 4, 6, 3, 7, 2].
• Chaque position est évaluée grâce à la fonction de performance, ici ce sera le nombre de
conflits. (b)
• Une phase de reproduction détermine quelles positions seront sélectionnées pour la
reproduction. Certaines positions peuvent être reproduites plusieurs fois, d'autres
disparaîtront. (c)
• Pour chaque paire se combinant, on détermine aléatoirement le point de croisement. (c)
• Les enfants sont créés en croisant chaque paire. (d)
• Finalement, chaque position subit éventuellement une mutation aléatoire. (e)
Commentaire
• Cette approche n'a (pour l'instant) pas donné de résultats intéressants. Elle ne fonctionne
qu'avec de petits damiers. Il semble difficile de choisir les différents paramètres.
Dans la version que nous allons voir, on ne tiendra pas compte de ces bonus, ni de la valeur des
lettres ; on se contentera de trouver tous les mots possibles, du plus long au plus court.
Voici le programme complet que nos allons décortiquer :
class Case:
def __init__(self, value):
self.value = value
self.neighbors = []
def getNeighbors(self):
# renvoie la liste des voisins d'une case
return self.neighbors
def getValue(self):
# renvoie la lettre d'une case
return self.value
# ----------------------------------------------------
def creer_trie(dico):
print("Lecture du dictionnaire")
fichier = open(dico, 'r')
liste_mots = fichier.readlines()
fichier.close()
print("Création du Trie")
trie = Trie()
trie.makeTrie(liste_mots)
print("Trie terminé")
return trie
def creer_connexions(grille):
# crée les listes d'adjacences du graphe
for m in range(16):
case = grille[m]
i, j = m//4, m%4
for n in range(16):
x, y = n//4, n%4
if abs(i-x)<=1 and abs(j-y)<=1 and m != n:
case.addNeighbor(grille[n])
if noeud.isWordEnd():
trouves.append(noeud.getLetter())
vus.append(case)
for voisin in case.getNeighbors():
if voisin not in vus:
results = chercher(noeud[voisin.getValue()], voisin, vus)
trouves.extend([noeud.getLetter()+ending for ending in results])
vus.remove(case)
return trouves
trie = creer_trie('ruzzle_dictionnaire.txt')
while True:
donnees = input("Entrez la grille ligne par ligne : ")
while len(donnees)!=16 and len(donnees)!=0:
donnees = input("Entrez la grille ligne par ligne : ")
if donnees=="": # taper 'return' pour sortir de la boucle infinie
break
donnees = donnees.lower()
grille = [Case(lettre) for lettre in donnees]
creer_connexions(grille)
resultats = []
for case in grille: # première lettre du mot à chercher
noeud = trie # on se place au début du Trie
mots_trouves = chercher(noeud[case.getValue()], case)
if len(mots_trouves) > 0:
resultats.extend(mots_trouves)
resultats = list(set(resultats)) # élimine les doublons
output = sorted(resultats, key=lambda mot: [len(mot)], reverse=True)
print(len(output),"mots trouvés")
print(output)
print()
On voit sur la première ligne que le programme importe la classe Trie du § 8.9.1. La procédure
creer_trie a déjà été utilisée au § 8.9.2
On a créé une classe case. Une case contient une lettre et la liste des lettres qui sont sur des
cases voisines (pas voisin, on entend qui jouxte horizontalement, verticalement ou en diagonale).
class Case:
def __init__(self, value):
self.value = value
self.neighbors = []
def getNeighbors(self):
# renvoie la liste des voisins d'une case
return self.neighbors
def getValue(self):
# renvoie la lettre d'une case
return self.value
La liste des lettres voisines d'une case est créée par la procédure creer_connexions(grille).
La grille est implémentée par une liste de 16 éléments.
Pour chacune des 16 lettres de la grille, on calcule sa ligne i et sa colonne j en fonction de la
position m dans grille. On parcourt ensuite les 15 autres cases de la grille et on calcule leur ligne x
et leur colonne y. Pour être voisin de cette case, il faut que |i-x| soit inférieur ou égal à 1 et de
même pour |j-y|.
def creer_connexions(grille):
# crée les listes d'adjacences du graphe
for m in range(16):
case = grille[m]
i, j = m//4, m%4
for n in range(16):
x, y = n//4, n%4
if abs(i-x)<=1 and abs(j-y)<=1 and m!=n:
case.addNeighbor(grille[n])
La fonction la plus délicate à analyser est évidemment chercher, car elle est récursive.
On a vu comment chercher un mot dans le trie (§ 8.9.2). Ici, le problème est un peu différent. En
effet, il ne serait pas très efficace de chercher toutes les suites de lettres possibles 1 et vérifier ensuite
lesquels sont des mots français dans le trie. Par expérience, il y a généralement entre 250 et 400 mots
valides dans une grille. On va faire l'inverse : on va parcourir le trie en utilisant les voisins.
On part du sommet du trie avec la première lettre de la grille (celle en haut à gauche).
On choisit son premier voisin dans la grille et on regarde si la suite de ces deux lettres existe dans
le trie.
Si la suite n'existe pas, on abandonne cette branche du trie, on marque ce voisin comme vu et on
essaie un autre voisin.
Si la suite existe, on regarde si c'est un mot complet. Si oui, on le mémorise. Si non, on prend un
voisin de la deuxième lettre et on analyse la suite des trois lettres. Et ainsi de suite…
On parcourt selon ce principe le trie 16 fois, car il y a 16 cases possibles pour la première lettre.
On élimine finalement les doublons dans la liste des mots trouvés et on affiche tous les mots du plus
long au plus court.
Exercice A9.6
A9.6
Modifiez le programme ci-dessus pour trouver la plus belle grille de Ruzzle grâce à un algorithme
probabiliste. Par « plus belle », on entend celle qui contient le plus de mots français.
Vous utiliserez le dictionnaire du chapitre 9 qui est disponible sur le site web compagnon :
https://www.apprendre-en-ligne.net/info/algo/
Voici les fréquences des lettres de ce dictionnaire (on a analysé tous les mots de 2 à 16 lettres
sans tiret et sans apostrophe) :
E S A I R N T O L U C M D
14.89% 10.21% 9.71% 9.40% 8.67% 7.34% 6.82% 5.82% 4.01% 3.60% 3.40% 2.54% 2.36%
P G B F H Z V Q Y X J K W
2.35% 1.60% 1.40% 1.36% 1.16% 1.07% 0.96% 0.50% 0.34% 0.25% 0.18% 0.05% 0.01%
Exercice A9.7
A9.7
Modifiez le programme de l'exercice A9.6 pour rechercher la meilleure grille de Ruzzle grâce à
une descente de plus grande pente.
Exercice A9.8
A9.8
Modifiez le programme de l'exercice A9.6 pour rechercher la meilleure grille de Ruzzle grâce à
une recherche avec tabous.
Exercice A9.9
A9.9
Modifiez le programme de l'exercice A9.6 pour rechercher la meilleure grille de Ruzzle grâce à
un recuit simulé.
Exercice A9.10
A9.10
Modifiez le programme de l'exercice A9.6 pour rechercher la meilleure grille de Ruzzle grâce à
un algorithme génétique.
Sources
[1] Wikipédia, « Méta-heuristique », <http://fr.wikipedia.org/wiki/Métaheuristique>
[2] Wikipédia, « Recuit simulé », <http://fr.wikipedia.org/wiki/Recuit_simulé>
[3] Wikipédia, « Algorithme génétique », <http://fr.wikipedia.org/wiki/Algorithme_génétique>