On donne tout de suite un exemple de graphe, en prenant le 3ème exemple de la Figure 1 du texte.
On va définir ce graphe, comme une liste d'arêtes, et plusieurs éclairages.
graphe1 = [(1,3), (3,2), (2,4), (2,6), (2,7), (4,5), (5,6), (6,7), (6,9), (7,8), (8,9)]
graphe1 = [ (u-1, v-1) for (u,v) in graphe1 ]
def nbsommets(graphe):
n = 0
for (u, v) in graphe:
if u > n or v > n: n = max(u, v)
return n + 1
nbsommets(graphe1)
9
Quatre exemples d'éclairages, deux satisfaisant donc l'un trivialement, et les deux autres non satisfaisants :
eclairage1_sat = [0, 1, 2, 3, 4, 5, 6, 7, 8] # trivialement valide car on éclaire tout
eclairage2_sat = [1, 2, 3, 5, 6, 8] # valide mais on éclaire pas tout
eclairage1_nonsat = [2, 4, 8]
eclairage2_nonsat = [1, 2, 3, 5, 6]
On va devoir implémenter des fonctions réalisants les tâches suivantes :
gauche
, droite
, lesdeux
est un éclairage valide,Ensuite pour l'initialisation de l'algorithm génétique il nous faudra :
Et puis pour chaque étape de l'algorithme génétique, on transformera les 100 individus
def est_eclairage(graphe, places_eclairees):
""" Cette fonction est en O(M + L) = O(M) où M est le nombre d'arêtes, et L le nombre de places éclairées (<= N <= M par connexité).
"""
n = nbsommets(graphe)
sont_eclairees = [ False for _ in range(n) ]
for p in places_eclairees:
sont_eclairees[p] = True
for (u, v) in graphe:
if not sont_eclairees[u] and not sont_eclairees[v]:
return False
return True
est_eclairage(graphe1, eclairage1_sat)
True
est_eclairage(graphe1, eclairage2_sat)
True
est_eclairage(graphe1, eclairage1_nonsat)
False
est_eclairage(graphe1, eclairage2_nonsat)
False
def places_moins_une(places_eclairees, place_a_enlever):
""" En O(L) si L est le nombre de places éclairées."""
return [place for place in places_eclairees if place != place_a_enlever]
def est_minimal(graphe, places_eclairees):
""" Cette fonction est en O(M L) où M est le nombre d'arêtes, et L le nombre de places éclairées.
"""
return est_eclairage(graphe, places_eclairees) and not(all([
est_eclairage(graphe, places_moins_une(places_eclairees, place_a_enlever))
for place_a_enlever in places_eclairees
]))
est_minimal(graphe1, eclairage1_sat)
False
est_minimal(graphe1, eclairage2_sat)
True
est_minimal(graphe1, eclairage1_nonsat)
False
est_minimal(graphe1, eclairage2_nonsat)
False
Si le graphe $G=(V,E)$ est donné par un tableau de ses rues $E = \{(u,v)\}$, on représente une liste de places éclairées $V'$ par un tableau de valeurs ternaires gauche
, droite
ou lesdeux
.
gauche, droite, lesdeux = "G", "D", "2"
def eclairage_vers_ternaires(graphe, places_eclairees):
""" O(M)"""
n = nbsommets(graphe)
ternaires = []
sont_eclairees = [ False for _ in range(n) ]
for p in places_eclairees:
sont_eclairees[p] = True
for (u,v) in graphe:
if sont_eclairees[u] and sont_eclairees[v]:
ternaires.append(lesdeux)
elif sont_eclairees[u]:
ternaires.append(gauche)
elif sont_eclairees[v]:
ternaires.append(droite)
return ternaires
def ternaires_vers_eclairage(graphe, ternaires):
""" O(M)"""
n = nbsommets(graphe)
places_eclairees = [ False for _ in range(n) ]
for (u,v), ternaire in zip(graphe, ternaires):
if ternaire == gauche or ternaire == lesdeux:
places_eclairees[u] = True
if ternaire == droite or ternaire == lesdeux:
places_eclairees[v] = True
# O(N)
eclairage = []
for i, p in enumerate(places_eclairees):
if p: eclairage.append(i)
return eclairage
def est_valide_ternaires(graphe, ternaires):
""" O(M)"""
return est_eclairage(graphe, ternaires_vers_eclairage(graphe, ternaires))
graphe1
[(0, 2), (2, 1), (1, 3), (1, 5), (1, 6), (3, 4), (4, 5), (5, 6), (5, 8), (6, 7), (7, 8)]
ternaires1_sat = eclairage_vers_ternaires(graphe1, eclairage1_sat)
ternaires1_sat
['2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2']
print(eclairage1_sat)
ternaires_vers_eclairage(graphe1, eclairage_vers_ternaires(graphe1, eclairage1_sat))
[0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
ternaires2_sat = eclairage_vers_ternaires(graphe1, eclairage2_sat)
ternaires2_sat
['D', '2', '2', '2', '2', 'G', 'D', '2', '2', 'G', 'D']
print(eclairage2_sat)
print(eclairage_vers_ternaires(graphe1, eclairage2_sat))
print(ternaires_vers_eclairage(graphe1, eclairage_vers_ternaires(graphe1, eclairage2_sat)))
print(est_valide_ternaires(graphe1, eclairage_vers_ternaires(graphe1, eclairage2_sat)))
[1, 2, 3, 5, 6, 8] ['D', '2', '2', '2', '2', 'G', 'D', '2', '2', 'G', 'D'] [1, 2, 3, 5, 6, 8] True
On a donc la "fonction de coût" recherchée :
def nb_places_eclairees(graphe, ternaires):
""" O(M)"""
eclairage = ternaires_vers_eclairage(graphe, ternaires)
return len(eclairage)
import random
def un_ternaire_aleatoire():
""" O(1)"""
return random.choice([gauche, droite, lesdeux])
def un_ternaire_aleatoire_different(valeur):
""" O(1)"""
if valeur == gauche:
return random.choice([droite, lesdeux])
elif valeur == droite:
return random.choice([gauche, lesdeux])
else:
return random.choice([gauche, droite])
def un_individu(graphe):
""" O(M)"""
return [un_ternaire_aleatoire() for (u,v) in graphe]
On peut facilement générer dix individus différents, qui sont tous des éclairages valides, et afficher leur coût :
for _ in range(10):
ternaires = un_individu(graphe1)
assert est_valide_ternaires(graphe1, ternaires)
cout = nb_places_eclairees(graphe1, ternaires)
print("L'éclairage", ternaires, "a un coût =", cout)
L'éclairage ['D', '2', '2', '2', 'D', 'D', '2', 'G', 'D', '2', '2'] a un coût = 8 L'éclairage ['2', 'D', 'G', 'G', 'G', 'D', '2', 'G', 'G', 'D', '2'] a un coût = 7 L'éclairage ['D', 'G', 'G', 'G', 'G', '2', 'G', 'D', '2', '2', '2'] a un coût = 8 L'éclairage ['D', 'G', 'D', '2', 'G', 'D', 'G', 'D', '2', 'G', 'G'] a un coût = 8 L'éclairage ['D', 'D', '2', 'G', '2', 'G', 'G', 'D', 'D', 'D', '2'] a un coût = 7 L'éclairage ['2', '2', 'D', 'D', 'D', '2', 'G', 'D', 'D', 'D', 'D'] a un coût = 9 L'éclairage ['D', 'G', '2', 'G', '2', '2', 'D', '2', 'G', 'D', '2'] a un coût = 8 L'éclairage ['2', 'D', 'D', '2', 'D', '2', 'G', 'G', 'D', 'G', '2'] a un coût = 9 L'éclairage ['D', 'D', 'D', '2', 'G', '2', 'D', '2', 'G', '2', 'G'] a un coût = 7 L'éclairage ['D', 'D', '2', '2', '2', 'D', '2', '2', 'D', '2', 'D'] a un coût = 8
def population_initiale(graphe, taille_population):
return [ un_individu(graphe) for _ in range(taille_population) ]
Par exemple, une population initiale de taille 5 est :
pop = population_initiale(graphe1, 5)
for individu in pop:
print("L'éclairage", individu, "a un coût =", nb_places_eclairees(graphe1, individu))
L'éclairage ['D', 'D', 'G', 'G', 'G', 'G', '2', 'G', '2', 'D', 'D'] a un coût = 7 L'éclairage ['D', 'D', '2', 'D', 'G', 'G', 'D', '2', 'G', '2', '2'] a un coût = 7 L'éclairage ['2', 'D', '2', '2', 'G', 'D', 'D', 'G', 'G', '2', 'G'] a un coût = 8 L'éclairage ['D', '2', 'G', 'D', 'G', 'G', 'D', 'D', '2', 'G', 'G'] a un coût = 7 L'éclairage ['D', 'G', 'D', '2', 'G', 'G', 'G', '2', 'G', 'G', 'G'] a un coût = 7
sorted(pop, key=lambda individu: nb_places_eclairees(graphe1, individu))
[['D', 'D', 'G', 'G', 'G', 'G', '2', 'G', '2', 'D', 'D'], ['D', 'D', '2', 'D', 'G', 'G', 'D', '2', 'G', '2', '2'], ['D', '2', 'G', 'D', 'G', 'G', 'D', 'D', '2', 'G', 'G'], ['D', 'G', 'D', '2', 'G', 'G', 'G', '2', 'G', 'G', 'G'], ['2', 'D', '2', '2', 'G', 'D', 'D', 'G', 'G', '2', 'G']]
On peut donc facilement trier des
On va écrire une fonction générique. Pour visualiser l'évolution de la population, plutôt que d'afficher une liste de 100 coûts, je préfère afficher un décompte du nombre d'individus ayant un certain coût, en Python cela se fait très facilement avec collections.Counter
:
import collections
collections.Counter([1,1,1,1,1,2,2,2,3])
Counter({1: 5, 2: 3, 3: 1})
def algorithme_genetique(
pop_init,
fct_cout,
muter_un,
croiser_deux,
taille_pop=100,
tau_meilleurs=0.48,
tau_cross=0.48,
nb_generations=1000,
):
""" Complexité en O(nb_generations * [
taille_pop * log(taille_pop) * C_cout
+ taille_pop * C_croisement
+ taille_pop * C_mutation
) où :
- C_cout est le coût de calcul de la fonction d'évaluation fct_cout,
- C_croisement est le coût de calcul de la fonction de croisement croiser_deux,
- C_mutation est le coût de calcul de la fonction de mutation muter_un,
"""
nb_meilleurs = int(tau_meilleurs * taille_pop)
nb_enfants = 2 * (int(tau_cross * taille_pop)//2) # nb paire !
nb_mutes = taille_pop - nb_meilleurs - nb_enfants
# première population
pop = pop_init(taille_pop)
# nb_generations étapes, toutes identiques
for generation in range(nb_generations):
couts = [fct_cout(sol) for sol in pop]
# bonus: affichage de la liste des couts
print("La génération numéro", generation, "a les coûts suivants :", collections.Counter(couts))
pop_triees = sorted(pop, key=fct_cout)
# 1) on prend les 48% meilleurs, laissés tels quels
meilleurs = pop_triees[:nb_meilleurs]
# 2) on prend les 48% moins bons, on les croise
moins_bons = pop_triees[-nb_enfants:]
enfants = [ ]
for i in range(len(moins_bons) // 2):
parent_1 = moins_bons[2*i]
parent_2 = moins_bons[2*i + 1]
enfant_1, enfant_2 = croiser_deux(parent_1, parent_2)
enfants.append(enfant_1)
enfants.append(enfant_2)
# 3) on prend les 4% meilleurs, et on les mute un peu
mutes = [ ]
for i in range(nb_mutes):
sain = meilleurs[i]
un_xmen = muter_un(sain)
mutes.append(un_xmen)
# on combine les trois listes en une nouvelle population
nouvelle_pop = meilleurs + enfants + mutes
pop = nouvelle_pop
# a la fin, on renvoie la meilleure solution
meilleure_solution = max(pop, key=fct_cout)
return meilleure_solution
On doit encore écrire les deux fonctions clés, muter_un
et croiser_deux
.
def une_mutation(graphe, ternaires):
position = random.randint(0, len(ternaires) - 1)
mute = [ t for t in ternaires ]
mute[position] = un_ternaire_aleatoire_different(mute[position])
return mute
def mutation(graphe, ternaires):
M = len(graphe)
nb_mutation = random.randint(1, M)
mute = une_mutation(graphe, ternaires)
for _ in range(nb_mutation - 1):
mute = une_mutation(graphe, mute)
return mute
graphe1
ternaires1_sat
[(0, 2), (2, 1), (1, 3), (1, 5), (1, 6), (3, 4), (4, 5), (5, 6), (5, 8), (6, 7), (7, 8)]
['2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2']
une_mutation(graphe1, ternaires1_sat)
['2', '2', '2', '2', '2', '2', 'D', '2', '2', '2', '2']
mutation(graphe1, ternaires1_sat)
['D', '2', 'D', 'D', '2', 'G', 'G', '2', '2', '2', 'D']
mutation(graphe1, ternaires1_sat)
['2', '2', 'G', '2', '2', '2', 'D', '2', '2', 'G', 'G']
mutation(graphe1, ternaires1_sat)
['2', '2', '2', 'G', '2', 'D', 'D', '2', 'D', '2', '2']
def croiser_deux_ternaires(graphe, ternaires_1, ternaires_2):
M1 = len(ternaires_1) // 2
M2 = len(ternaires_2) // 2
enfant_1 = ternaires_1[:M1] + ternaires_2[M2:]
enfant_2 = ternaires_1[M1:] + ternaires_2[:M2]
return enfant_1, enfant_2
print("Les deux parents suivants :")
ternaires_1 = mutation(graphe1, ternaires1_sat)
print(ternaires_1)
ternaires_2 = mutation(graphe1, ternaires1_sat)
print(ternaires_2)
enfant_1, enfant_2 = croiser_deux_ternaires(graphe1, ternaires_1, ternaires_2)
print("peuvent par exemple donner les deux enfants suivants :")
print(enfant_1)
print(enfant_2)
Les deux parents suivants : ['2', 'D', '2', '2', '2', '2', '2', '2', '2', '2', 'G'] ['2', '2', '2', 'D', 'G', '2', 'D', 'D', '2', '2', '2'] peuvent par exemple donner les deux enfants suivants : ['2', 'D', '2', '2', '2', '2', 'D', 'D', '2', '2', '2'] ['2', '2', '2', '2', '2', 'G', '2', '2', '2', 'D', 'G']
On assemble le tout :
def eclairage_genetique(graphe,
taille_pop=100,
tau_meilleurs=0.48,
tau_cross=0.48,
nb_generations=50,
):
""" Complexité en O(nb_generations * [
taille_pop * log(taille_pop) * O(M)
+ taille_pop * O(M)
+ taille_pop * O(M)
) = O (nb_generations * taille_pop * log(taille_pop) * M) où :
- M est le nombre d'arêtes dans le graphe.
Donc si nb_generations et taille_pop sont constantes, cette fonction est en O(M).
"""
# on définit les quatre fonctions, pour ce graphe
def pop_init(taille_pop):
return population_initiale(graphe, taille_pop)
def fct_cout(individu):
return nb_places_eclairees(graphe, individu)
def muter_un(individu):
return mutation(graphe, individu)
def croiser_deux(parent_1, parent_2):
return croiser_deux_ternaires(graphe, parent_1, parent_2)
# on appelle la fonction générique
return algorithme_genetique(
pop_init,
fct_cout,
muter_un,
croiser_deux,
taille_pop=taille_pop,
tau_meilleurs=tau_meilleurs,
tau_cross=tau_cross,
nb_generations=nb_generations,
)
Et on donne un exemple :
eclairage_genetique(graphe1)
La génération numéro 0 a les coûts suivants : Counter({9: 36, 8: 34, 7: 26, 6: 4}) La génération numéro 1 a les coûts suivants : Counter({8: 34, 7: 32, 9: 29, 6: 5}) La génération numéro 2 a les coûts suivants : Counter({7: 35, 9: 30, 8: 28, 6: 7}) La génération numéro 3 a les coûts suivants : Counter({7: 38, 9: 29, 8: 23, 6: 10}) La génération numéro 4 a les coûts suivants : Counter({7: 45, 9: 28, 8: 17, 6: 10}) La génération numéro 5 a les coûts suivants : Counter({7: 48, 9: 27, 8: 13, 6: 12}) La génération numéro 6 a les coûts suivants : Counter({7: 45, 9: 24, 8: 18, 6: 13}) La génération numéro 7 a les coûts suivants : Counter({7: 41, 9: 23, 8: 22, 6: 14}) La génération numéro 8 a les coûts suivants : Counter({7: 39, 8: 25, 9: 22, 6: 14}) La génération numéro 9 a les coûts suivants : Counter({7: 40, 9: 26, 8: 19, 6: 15}) La génération numéro 10 a les coûts suivants : Counter({7: 39, 9: 23, 8: 21, 6: 17}) La génération numéro 11 a les coûts suivants : Counter({7: 40, 9: 26, 6: 20, 8: 14}) La génération numéro 12 a les coûts suivants : Counter({7: 41, 6: 23, 9: 22, 8: 14}) La génération numéro 13 a les coûts suivants : Counter({7: 33, 6: 26, 8: 20, 9: 20, 5: 1}) La génération numéro 14 a les coûts suivants : Counter({7: 30, 6: 28, 9: 23, 8: 17, 5: 2}) La génération numéro 15 a les coûts suivants : Counter({7: 30, 6: 29, 9: 26, 8: 13, 5: 2}) La génération numéro 16 a les coûts suivants : Counter({6: 31, 8: 25, 7: 24, 9: 18, 5: 2}) La génération numéro 17 a les coûts suivants : Counter({6: 33, 7: 24, 9: 24, 8: 17, 5: 2}) La génération numéro 18 a les coûts suivants : Counter({6: 34, 8: 22, 9: 21, 7: 20, 5: 3}) La génération numéro 19 a les coûts suivants : Counter({6: 35, 9: 22, 7: 21, 8: 19, 5: 3}) La génération numéro 20 a les coûts suivants : Counter({6: 37, 7: 21, 9: 21, 8: 18, 5: 3}) La génération numéro 21 a les coûts suivants : Counter({6: 39, 9: 23, 8: 20, 7: 15, 5: 3}) La génération numéro 22 a les coûts suivants : Counter({6: 41, 9: 24, 8: 19, 7: 13, 5: 3}) La génération numéro 23 a les coûts suivants : Counter({6: 43, 9: 23, 8: 16, 7: 14, 5: 4}) La génération numéro 24 a les coûts suivants : Counter({6: 43, 8: 19, 9: 19, 7: 15, 5: 4}) La génération numéro 25 a les coûts suivants : Counter({6: 46, 9: 19, 8: 16, 7: 15, 5: 4}) La génération numéro 26 a les coûts suivants : Counter({6: 44, 8: 25, 9: 15, 7: 12, 5: 4}) La génération numéro 27 a les coûts suivants : Counter({6: 47, 9: 22, 8: 15, 7: 12, 5: 4}) La génération numéro 28 a les coûts suivants : Counter({6: 44, 9: 21, 8: 19, 7: 11, 5: 5}) La génération numéro 29 a les coûts suivants : Counter({6: 45, 9: 22, 8: 15, 7: 13, 5: 5}) La génération numéro 30 a les coûts suivants : Counter({6: 46, 9: 23, 8: 14, 7: 12, 5: 5}) La génération numéro 31 a les coûts suivants : Counter({6: 44, 9: 21, 8: 18, 7: 12, 5: 5}) La génération numéro 32 a les coûts suivants : Counter({6: 46, 9: 23, 8: 18, 7: 8, 5: 5}) La génération numéro 33 a les coûts suivants : Counter({6: 44, 8: 24, 9: 20, 7: 7, 5: 5}) La génération numéro 34 a les coûts suivants : Counter({6: 43, 9: 29, 8: 12, 7: 11, 5: 5}) La génération numéro 35 a les coûts suivants : Counter({6: 45, 9: 23, 8: 18, 7: 9, 5: 5}) La génération numéro 36 a les coûts suivants : Counter({6: 47, 9: 26, 8: 15, 7: 7, 5: 5}) La génération numéro 37 a les coûts suivants : Counter({6: 44, 9: 22, 8: 20, 7: 9, 5: 5}) La génération numéro 38 a les coûts suivants : Counter({6: 44, 8: 22, 9: 20, 7: 9, 5: 5}) La génération numéro 39 a les coûts suivants : Counter({6: 45, 9: 21, 8: 21, 7: 8, 5: 5}) La génération numéro 40 a les coûts suivants : Counter({6: 47, 9: 25, 8: 13, 7: 10, 5: 5}) La génération numéro 41 a les coûts suivants : Counter({6: 45, 9: 26, 8: 17, 7: 7, 5: 5}) La génération numéro 42 a les coûts suivants : Counter({6: 45, 9: 25, 8: 14, 7: 9, 5: 7}) La génération numéro 43 a les coûts suivants : Counter({6: 44, 9: 21, 8: 16, 7: 11, 5: 8}) La génération numéro 44 a les coûts suivants : Counter({6: 42, 9: 22, 8: 18, 7: 10, 5: 8}) La génération numéro 45 a les coûts suivants : Counter({6: 44, 9: 23, 8: 16, 7: 9, 5: 8}) La génération numéro 46 a les coûts suivants : Counter({6: 43, 8: 21, 9: 18, 7: 10, 5: 8}) La génération numéro 47 a les coûts suivants : Counter({6: 41, 8: 22, 9: 19, 7: 10, 5: 8}) La génération numéro 48 a les coûts suivants : Counter({6: 43, 9: 25, 8: 15, 7: 9, 5: 8}) La génération numéro 49 a les coûts suivants : Counter({6: 42, 9: 21, 8: 17, 7: 12, 5: 8})
['2', '2', '2', 'G', 'G', 'G', 'G', '2', 'D', 'D', 'G']
On a trouvé un éclairage valide avec seulement 5 places éclairées, en partant d'une population qui avait des coûts entre 7 et 9.
Si vous êtes curieux, je vous laisse travailler davantage sur ça.