diff --git a/.github/workflows/check-urls.yml b/.github/workflows/check-urls.yml index 407be41f..15b236ff 100644 --- a/.github/workflows/check-urls.yml +++ b/.github/workflows/check-urls.yml @@ -42,6 +42,6 @@ jobs: print_all: false timeout: 5 retry_count# : 3 - exclude_urls: https://hal.archives-ouvertes.fr/hal-00990252/document,https://github.com/onnx/models/raw/main/vision/classification/mobilenet/model/mobilenetv2-12.onnx,https://arxiv.org/ftp/arxiv/papers/1510/1510.04863.pdf,https://hal.science/hal-01125940,https://www.data.gouv.fr/fr/datasets/r/63352e38-d353-4b54-bfd1-f1b3ee1cabd7,https://github.com/sdpython/teachpyx/raw/main/_data/examen2021.zip + exclude_urls: https://hal.archives-ouvertes.fr/hal-00990252/document,https://github.com/onnx/models/raw/main/vision/classification/mobilenet/model/mobilenetv2-12.onnx,https://arxiv.org/ftp/arxiv/papers/1510/1510.04863.pdf,https://hal.science/hal-01125940,https://www.data.gouv.fr/fr/datasets/r/63352e38-d353-4b54-bfd1-f1b3ee1cabd7,https://github.com/sdpython/teachpyx/raw/main/_data/examen2021.zip,https://www.enseignement.polytechnique.fr/informatique/INF423/uploads/Main/poly-good.pdf exclude_patterns: https://www.data.gouv.fr/fr/datasets/r/e3d83ab3-dc52-4c99-abaf-8a38050cc68c,https://github.com/onnx/models/raw/main/vision/classification/mobilenet/model/mobilenetv2-12.onnx,https://www.data.gouv.fr/fr/datasets/r/63352e38-d353-4b54-bfd1-f1b3ee1cabd7,https://github.com/sdpython/teachpyx/raw/main/_data/examen2021.zip # force_pass : true diff --git a/_doc/articles/2023-08-09-hermionne.rst b/_doc/articles/2023-08-09-hermionne.rst new file mode 100644 index 00000000..711168f2 --- /dev/null +++ b/_doc/articles/2023-08-09-hermionne.rst @@ -0,0 +1,26 @@ + + +.. index:: Hermionne, Harry Potter, énigme + +.. _l-enigme-hermionne: + +2023-08-09 : l'énigme d'Hermionne +================================= + +C'est une énigme que le personnage d'Hermionne résoud dans le +premier tome de Harry Potter. + +#. Il y a trois fioles de poison, deux fioles de vin d'ortie, + une fiole permettant d'avancer et une fiole permettant de reculer. +#. Immédiatement à gauche de chacune des deux fioles de vin se + trouve une fiole de poison. +#. Les fioles 1 et 7 ont des contenus différents ; + ni l'une ni l'autre n'est la fiole qui permet d'avancer. +#. Ni la fiole la plus grande (fiole 6) ni la plus petite (fiole 3) + ne contient du poison. +#. Les contenus des fioles 2 et 6 sont identiques. + +7 fioles et beaucoup de permutations. Tout l'enjeu consiste +à éliminer le plus de permutations jusqu'à n'en garder qu'une seule. +Il n'y a rien d'insurmontable pour un ordinateur et cette énigme +peut se résoudre de la même façon que :ref:`l-exemple-einstein-riddle`. diff --git a/_doc/articles/index.rst b/_doc/articles/index.rst index d95edce1..487e9fd8 100644 --- a/_doc/articles/index.rst +++ b/_doc/articles/index.rst @@ -5,5 +5,6 @@ Collections d'articles inclassables .. toctree:: + 2023-08-09-hermionne 2023-08-03-code-jam 2022-12-07-cartopy diff --git a/_doc/examples/plot_einstein_riddle.py b/_doc/examples/plot_einstein_riddle.py new file mode 100644 index 00000000..e3560abd --- /dev/null +++ b/_doc/examples/plot_einstein_riddle.py @@ -0,0 +1,535 @@ +# coding: utf-8 +""" +.. _l-exemple-einstein-riddle: + +==================================== +L'énigme d'Einstein et sa résolution +==================================== + +Résolution de l'énigme `L'énigme d'Einstein +`_. +Implémentatin d'une solution à base de règles. + +Je la reproduis ici : + +Il y a cinq maisons de cinq couleurs différentes. Dans chacune de ces maisons, +vit une personne de nationalité différente. +Chacune de ces personnes boit une boisson différente, +fume un cigare différent et a un animal domestique différent. + +1. L'Anglais vit dans la maison rouge. +2. Le Suédois a des chiens. +3. Le Danois boit du thé. +4. La maison verte est à gauche de la maison blanche. +5. Le propriétaire de la maison verte boit du café. +6. La personne qui fume des Pall Mall a des oiseaux. +7. Le propriétaire de la maison jaune fume des Dunhill. +8. La personne qui vit dans la maison du centre boit du lait. +9. Le Norvégien habite dans la première maison. +10. L'homme qui fume des Blend vit à côté de celui qui a des chats. +11. L'homme qui a un cheval est le voisin de celui qui fume des Dunhill. +12. Le propriétaire qui fume des Blue Master boit de la bière. +13. L'Allemand fume des prince. +14. Le Norvégien vit juste à côté de la maison bleue. +15. L'homme qui fume des Blend a un voisin qui boit de l'eau. + +**Question : Qui a le poisson ?** + +Après quelques essais, une bonne feuille de papier, on arrive à +reconstituer la solution après de nombreuses déductions logiques +et quelques essais. On peut voir aussi ce jeu comme un puzzle : +chaque configuration est un pièce du puzzle dont la forme des bords +est définie par toutes ces règles. Il faut trouver le seul emboîtement +possible sachant que parfois, une pièce peut s'emboîter avec plusieurs +mais qu'il n'existe qu'une façon de les emboîter toutes ensemble. +Ecrire un programme qui résoud ce problème revient à s'intéresser à deux problèmes : + +1. Comment définir une pièce du puzzle ? +2. Comment parcourir toutes les combinaisons possibles ? + +Chaque règle ou pièce de puzzle peut être exprimer comme une +`clause `_. +Pour notre problème, chaque pièce du puzzle est simplement décrite par +un attribut (rouge, norvégien) et un numéro de maisons (1 à 5). +Les règles définissent la compatibilité de deux pièces. On peut +regrouper ces règles en cinq catégories : + +1. Un attribut est à la position p (règle 9). +2. Deux attributs sont équivalents (règle 1). +3. Deux attributs sont voisins (règle 11). +4. Deux attributs sont ordonnés par rapport aux positions (règle 4). +5. Deux attributs font partie du même ensemble et sont exclusives : + on ne peut pas être l'un et l'autre à la fois (rouge et jaune par exemple). + +Une fois que chaque règle a été exprimée dans une de ces cinq catégories, +il faut définir l'association de deux règles (ou clause) pour +former une clause plus complexe. Trois cas possibles : + +1. Deux clauses sont compatibles : on peut avoir l'une et l'autre. +2. Deux clauses sont incompatibles : on ne peut avoir l'une et l'autre. + +Dans le premier cas, la clause résultante est simplement qu'on peut la clause A +et la clause B : :math:*A* et *B*. Dans le second cas, il existe deux +possibilités, on peut avoir l'une et l'opposé de l'autre ou l'inverse : +:math:`(A \, et\, non \, B) \, ou\, (non \, A \, et\, B)`. + +Avec cette description, il est plus facile d'exprimer le problème avec des +objets informatiques ce que fait le programme suivant. Il explicite ensuite +toutes les configurations compatibles avec une règle donnée +(mais pas toutes ensembles). + +.. note:: + + L'énigme d'Einstein est une énigme comme celle que résoud + Hermionne dans le premier tome de Harry Potter + (voir :ref:`l-enigme-hermionne`). + + +On commence par la fonction `permutation`: +qui énumère les permutations d'un ensemble : +""" +import copy +from io import StringIO +import pandas + + +########################## +# Fonction permutation +# ==================== + + +def permutation(nb): + per = [] + p = [i for i in range(0, nb)] + while p[0] < nb: + cont = False + for i in range(1, nb): + if p[i] in p[0:i]: + cont = True + break + + if not cont: + per.append(copy.copy(p)) + + p[nb - 1] += 1 + for j in range(nb - 1, 0, -1): + if p[j] >= nb: + p[j] = 0 + p[j - 1] += 1 + + return per + + +########################################## +# La classe Rule +# ============== + + +class Rule: + """ + This class defines a constraint of the problem or a clause. + + There are 5 different types of clauses described by Einstein's enigma + each of them is described by a different class. + """ + + def __init__(self): + # name of the rule + self.name = None + # set of clauses + self.set = None + + def genere(self): + """ + Generates all possible clauses (list of lists) + (`l[0][0]` et `l[0][1]`) ou (`l[1][0]` et `l[1][1]`), + a clause is a triplet of + `(person, (property, category))`. + """ + return None + + def __str__(self): + """ + display + """ + if self.name is not None: + if "clauses" not in self.__dict__: + s = self.name + " \t: " + a = self.genere() + for al in a: + st = "\n ou " + str(al) + if len(st) > 260: + st = st[:260] + "..." + s += st + if len(s) > 1000: + break + return s + else: + s = self.name + " \t: " + str(self.set) + for al in self.clauses: + st = "\n ou " + str(al) + if len(st) > 260: + st = st[:260] + "..." + s += st + if len(s) > 1000: + break + return s + return "None" + + def combine(self, cl1, cl2): + """ + Combines two clauses, two cases: + + 1. nothing in common or everything in common --> concatenation of clauses + 2. a position or a property in common --> null clause + + :param cl1: clause 1 + :param cl2: clause 2 + :return: the new clause + + A clause is a `Rule`. + """ + # incompatibility + for p1 in cl1: + for p2 in cl2: + if p1[1][0] == p2[1][0]: # same property + if p1[0] != p2[0]: # but different positions + return None + if p1[0] == p2[0]: # same person + if p1[1][1] == p2[1][1] and p1[1][0] != p2[1][0]: + # same category but different properties + return None + # compatibility + r = copy.deepcopy(cl1) + for c in cl2: + if c not in r: + r.append(c) + return r + + def combine_cross_sets(self, set1, set2): + """ + Combines two sets of clauses. + + :param set1: set of clauses 1 + :param set2: set of clauses 2 + :return: combination + """ + if len(set1) == 0: + return copy.deepcopy(set2) + if len(set2) == 0: + return copy.deepcopy(set1) + res = [] + for cl1 in set1: + for cl2 in set2: + r = self.combine(cl1, cl2) + if r is not None: + res.append(r) + return res + + +################################### +# Explicit rules +# ============== + + +class RulePosition(Rule): + """ + p1 at position + """ + + def __init__(self, p1, pos): + self.set = [p1] + self.name = "position" + self.position = pos + + def genere(self): + """ + overrides method ``genere`` + """ + return [[(self.position, self.set[0])]] + + +class RuleEquivalence(Rule): + """ + p1 equivalent to p2 + """ + + def __init__(self, p1, p2): + self.set = [p1, p2] + self.name = "equivalence" + + def genere(self): + """ + overrides method ``genere`` + """ + li = [] + for i in range(0, 5): + li.append([(i, self.set[0]), (i, self.set[1])]) + return li + + +class RuleVoisin(Rule): + """ + p1 and p2 are neighbors + """ + + def __init__(self, p1, p2): + self.set = [p1, p2] + self.name = "voisin" + + def genere(self): + """ + overrides method ``genere`` + """ + li = [] + for i in range(0, 4): + li.append([(i, self.set[0]), (i + 1, self.set[1])]) + li.append([(i + 1, self.set[0]), (i, self.set[1])]) + return li + + +class RuleAvant(Rule): + """ + p1 before p2 + """ + + def __init__(self, p1, p2): + self.set = [p1, p2] + self.name = "avant" + + def genere(self): + """ + overrides method ``genere`` + """ + li = [] + for j in range(1, 5): + for i in range(0, j): + li.append([(i, self.set[0]), (j, self.set[1])]) + return li + + +class RuleEnsemble(Rule): + """ + permutation of the elements of a category + """ + + def __init__(self, set, categorie): + self.set = [(s, categorie) for s in set] + self.name = "ensemble" + + def genere(self): + """ + overrides method ``genere`` + """ + li = [] + per = permutation(5) + for p in per: + tl = [] + for i in range(0, len(p)): + tl.append((i, self.set[p[i]])) + li.append(tl) + return li + + +############################## +# Description du problème avec ce code +# ==================================== + + +def find(p): + for i in range(0, len(ensemble)): + if p in ensemble[i]: + return (p, i) + return None + + +ttcouleur = ["jaune", "bleu", "rouge", "blanc", "vert"] +ttnationalite = ["danois", "norvegien", "anglais", "allemand", "suedois"] +ttboisson = ["eau", "the", "lait", "cafe", "biere"] +ttcigare = ["Dunhill", "Blend", "Pall Mall", "Prince", "Bluemaster"] +ttanimal = ["chats", "cheval", "oiseaux", "poisson", "chiens"] +ensemble = [ttcouleur, ttnationalite, ttboisson, ttcigare, ttanimal] + +####################### +# Les règles. + +regle = [] + +regle.append(RulePosition(find("lait"), 2)) +regle.append(RulePosition(find("norvegien"), 0)) + +regle.append(RuleEquivalence(find("Pall Mall"), find("oiseaux"))) +regle.append(RuleEquivalence(find("anglais"), find("rouge"))) +regle.append(RuleEquivalence(find("suedois"), find("chiens"))) +regle.append(RuleEquivalence(find("danois"), find("the"))) +regle.append(RuleEquivalence(find("vert"), find("cafe"))) +regle.append(RuleEquivalence(find("jaune"), find("Dunhill"))) +regle.append(RuleEquivalence(find("biere"), find("Bluemaster"))) +regle.append(RuleEquivalence(find("allemand"), find("Prince"))) + +regle.append(RuleVoisin(find("Dunhill"), find("cheval"))) +regle.append(RuleVoisin(find("norvegien"), find("bleu"))) +regle.append(RuleVoisin(find("Blend"), find("eau"))) +regle.append(RuleVoisin(find("Blend"), find("chats"))) + +regle.append(RuleAvant(find("vert"), find("blanc"))) + +regle.append(RuleEnsemble(ttcouleur, 0)) +regle.append(RuleEnsemble(ttnationalite, 1)) +regle.append(RuleEnsemble(ttboisson, 2)) +regle.append(RuleEnsemble(ttcigare, 3)) +regle.append(RuleEnsemble(ttanimal, 4)) + + +for r in regle: + print(r) + +################################## +# Parmi tous ces cas possibles, beaucoup sont incompatibles. +# L'objectif est d'éliminer tous ceux qui sont incompatibles pour ne +# garer que les 25 qui constituent la solution. L'algorithme est inspiré de la +# `logique des prédicats +# `_`. +# De manière récursive, la fonction ``solve`` combine +# les clauses jusqu'à ce qu'il ne puisse plus continuer : +# +# 1. Soit le même attribut apparaît à deux positions différentes : incompatibilité. +# 2. Soit deux attributs apparaissent à la même position : incompatibilité. +# 3. Soit il ne reste plus qu'une seule clause : c'est la solution. + + +class Enigma: + """ + This class solves the enigma. + We describe the enigma using the classes we defined above. + + :param display: if True, use print to print some information + """ + + def __init__(self, display=True): + self.regle = [] + + self.regle.append(RulePosition(self.find("lait"), 2)) + self.regle.append(RulePosition(self.find("norvegien"), 0)) + + self.regle.append(RuleEquivalence(self.find("Pall Mall"), self.find("oiseaux"))) + self.regle.append(RuleEquivalence(self.find("anglais"), self.find("rouge"))) + self.regle.append(RuleEquivalence(self.find("suedois"), self.find("chiens"))) + self.regle.append(RuleEquivalence(self.find("danois"), self.find("the"))) + self.regle.append(RuleEquivalence(self.find("vert"), self.find("cafe"))) + self.regle.append(RuleEquivalence(self.find("jaune"), self.find("Dunhill"))) + self.regle.append(RuleEquivalence(self.find("biere"), self.find("Bluemaster"))) + self.regle.append(RuleEquivalence(self.find("allemand"), self.find("Prince"))) + + self.regle.append(RuleVoisin(self.find("Dunhill"), self.find("cheval"))) + self.regle.append(RuleVoisin(self.find("norvegien"), self.find("bleu"))) + self.regle.append(RuleVoisin(self.find("Blend"), self.find("eau"))) + self.regle.append(RuleVoisin(self.find("Blend"), self.find("chats"))) + + self.regle.append(RuleAvant(self.find("vert"), self.find("blanc"))) + + self.regle.append(RuleEnsemble(ttcouleur, 0)) + self.regle.append(RuleEnsemble(ttnationalite, 1)) + self.regle.append(RuleEnsemble(ttboisson, 2)) + self.regle.append(RuleEnsemble(ttcigare, 3)) + self.regle.append(RuleEnsemble(ttanimal, 4)) + + for r in self.regle: + r.clauses = r.genere() + r.utilise = False + + self.count = 0 + + def find(self, p): + """ + Finds a clause in the different sets of clause (houses, colors, ...). + + :param p: clause + :return: tuple (clause, position) + """ + for i in range(0, len(ensemble)): + if p in ensemble[i]: + return (p, i) + return None + + def to_dataframe(self): + sr = [] + matrix = [list(" " * 5) for _ in range(0, 5)] + for row in self.solution: + i = row[0] + j = row[1][1] + s = row[1][0] + matrix[i][j] = s + for row in matrix: + sr.append(", ".join(row)) + text = "\n".join(sr) + return pandas.read_csv(StringIO(text), header=None) + + def solve(self, solution=[], logf=print): # solution = [ ]) : + """ + Solves the enigma by eploring in deepness, + the method is recursive + + :param solution: `[]` empty at the beginning, recursively used then + :return: solution + """ + + self.count += 1 + if self.count % 10 == 0: + logf("*", self.count, " - properties in place : ", len(solution) - 1) + + if len(solution) == 25: + # we know the solution must contain 25 clauses, + # if are here than the problem is solved unless some incompatibility + for r in self.regle: + cl = r.combine_cross_sets([solution], r.clauses) + if cl is None or len(cl) == 0: + # the solution is incompatible with a solution + return None + self.solution = solution + return solution + + # we are looking for the rule which generates the least possible clauses + # in order to reduce the number of possibilities as much as possible + # the research could be represented as a tree, we avoid creating two many paths + best = None + rule = None + + for r in self.regle: + cl = r.combine_cross_sets([solution], r.clauses) + + if cl is None: + # the solution is incompatible with a solution + return None + + # we check rule r is bringing back some results + for c in cl: + if len(c) > len(solution): + break + else: + cl = None + + if cl is not None and (best is None or len(best) > len(cl)): + best = cl + rule = r + + if best is None: + # the solution is incompatible with a solution + return None + + rule.utilise = True + + # we test all clauses + for c in best: + r = self.solve(c, logf=logf) + if r is not None: + # we found + return r + + rule.utilise = False # impossible + return None + + +en = Enigma() +en.solve() +print(en.to_dataframe()) diff --git a/_doc/examples/plot_tsp.py b/_doc/examples/plot_tsp.py new file mode 100644 index 00000000..2ea18a2d --- /dev/null +++ b/_doc/examples/plot_tsp.py @@ -0,0 +1,361 @@ +# coding: utf-8 +""" +============================================= +Réflexions autour du voyage de commerce (TSP) +============================================= + +Le `problème du voyageur de commerce +`_ +consiste à trouver le plus court chemin passant par toutes les villes. +On parle aussi de `circuit hamiltonien `_ +qui consiste à trouver le plus court chemin passant par tous les noeuds d'un graphe. +Ce programme explore quelques solutions approchées et intuitives. + +Ce problème est `NP-complet `_ +à savoir qu'il n'existe pas d'algorithme qui permette de trouver la solution avec un +coût polynômial. C'est aussi un problème différent du `plus court chemin dans un graphe +`_ +qui consiste à trouver le plus court chemin reliant deux noeuds d'un graphe +(mais pas forcément tous les noeuds de ce graphe). + +Des villes tirées au hasard +=========================== +""" + +import random +import matplotlib.pyplot as plt + +n = 30 +x = [random.random() for _ in range(n)] +y = [random.random() for _ in range(n)] + +plt.plot(x, y, "o") + + +######################################## +# Un parcours aléatoire de tous les noeuds de graphe +# donnera quelque chose de très éloigné de la solution optimale : + +plt.plot(x + [x[0]], y + [y[0]], "o-") + + +############################################# +# Croisements +# =========== +# +# La première constation est que le chemin ne peut pas être optimal +# car des arcs se croisent. On en déduit qu'une façon d'améliorer ce +# chemin est de *décroiser* certaines parties. On peut par exemple +# choisir deux points au hasard, retourner la partie du chemin au milieu +# de ces deux points et voir si la longueur du chemin s'en trouve diminuée. +# On peut également parcourir toutes les paires de noeuds possibles. +# C'est ce qui est implémenté ci-dessous. + + +def longueur(x, y, ordre): + i = ordre[-1] + x0, y0 = x[i], y[i] + d = 0 + for o in ordre: + x1, y1 = x[o], y[o] + d += (x0 - x1) ** 2 + (y0 - y1) ** 2 + x0, y0 = x1, y1 + return d + + +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + +#################################### +# Permutations. + + +def permutation(x, y, ordre): + d = longueur(x, y, ordre) + d0 = d + 1 + it = 1 + while d < d0: + it += 1 + print("iteration", it, "d=", d) + d0 = d + for i in range(0, len(ordre) - 1): + for j in range(i + 2, len(ordre)): + r = ordre[i:j].copy() + r.reverse() + ordre2 = ordre[:i] + r + ordre[j:] + t = longueur(x, y, ordre2) + if t < d: + d = t + ordre = ordre2 + return ordre + + +ordre = permutation(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") + +####################################################### +# Voilà qui est mieux. Maintenant, supposons que nous faisons une +# erreur lors du calcul de la distance : nous oublions le dernier +# arc qui boucle le chemin du dernier noeud au premier. + + +def longueur(x, y, ordre): + # on change cette fonction + d = 0 + for i in range(1, len(ordre)): + n = ordre[i - 1] + o = ordre[i] + x0, y0 = x[n], y[n] + x1, y1 = x[o], y[o] + d += (x0 - x1) ** 2 + (y0 - y1) ** 2 + return d + + +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + +############################################# +# Et graphiquement. + +ordre = permutation(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre] +yo = [y[o] for o in ordre] +plt.plot(xo, yo, "o-") + + +################################################# +# Noeud de départ constant +# ======================== +# +# Jusque ici, tout concorde. Le chemin est plus court en ce sens qu'il +# oublie délibérément l'arc de bouclage que l'algorithme a tendance à +# choisir grand. Pour gagner du temps de calcul, un développeur se dit +# que le noeud de départ peut être constant. Après tout, le chemin est +# une boucle, elle passera toujours par le premier noeud. Qu'il soit en +# première position ne change rien et puis inverser une moitié, c'est +# équivalent à inverser l'autre moitié. On fait donc juste une modification : + + +def longueur(x, y, ordre): + i = ordre[-1] + x0, y0 = x[i], y[i] + d = 0 + for o in ordre: + x1, y1 = x[o], y[o] + d += (x0 - x1) ** 2 + (y0 - y1) ** 2 + x0, y0 = x1, y1 + return d + + +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + + +def permutation(x, y, ordre): + d = longueur(x, y, ordre) + d0 = d + 1 + it = 1 + while d < d0: + it += 1 + print("iteration", it, "d=", d, "ordre[0]", ordre[0]) + d0 = d + for i in range( + 1, len(ordre) - 1 + ): # on part de 1 et plus de 0, on est sûr que le premier noeud ne bouge pas + for j in range(i + 2, len(ordre)): + r = ordre[i:j].copy() + r.reverse() + ordre2 = ordre[:i] + r + ordre[j:] + t = longueur(x, y, ordre2) + if t < d: + d = t + ordre = ordre2 + return ordre + + +ordre = permutation(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") +plt.text(xo[0], yo[0], "0", color="r", weight="bold", size="x-large") +plt.text(xo[-2], yo[-2], "N-1", color="r", weight="bold", size="x-large") + +#################################################### +# Le résultat attendu n'est pas celui qu'on observe. +# Est-ce une erreur d'implémentation ou +# une erreur de raisonnement ? J'étais pourtant sûr que mon raisonnement était correct +# et j'aurais tort d'en douter. C'est une erreur d'implémentation. +# Lorsqu'on``for j in range(i+2,len(ordre)):`` et ``r = ordre[i:j].copy()``, +# on écrit que ``j`` va de ``i+2`` inclus à ``len(ordre)`` exclu. Puis +# lorsqu'on écrit ``ordre[i:j]``, l'indice ``j`` est exclu ! Autrement dit, +# dans cette implémentation, le premier noeud et le dernier noeud ne bougeront +# jamais ! On s'empresse de corriger cela. + + +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + + +def permutation(x, y, ordre): + d = longueur(x, y, ordre) + d0 = d + 1 + it = 1 + while d < d0: + it += 1 + print("iteration", it, "d=", d, "ordre[0]", ordre[0]) + d0 = d + for i in range( + 1, len(ordre) - 1 + ): # on part de 1 et plus de 0, on est sûr que le premier noeud ne bouge pas + for j in range(i + 2, len(ordre) + 1): # correction ! + r = ordre[i:j].copy() + r.reverse() + ordre2 = ordre[:i] + r + ordre[j:] + t = longueur(x, y, ordre2) + if t < d: + d = t + ordre = ordre2 + return ordre + + +ordre = permutation(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") +plt.text(xo[0], yo[0], "0", color="r", weight="bold", size="x-large") +plt.text(xo[-2], yo[-2], "N-1", color="r", weight="bold", size="x-large") + + +######################################################### +# Pas parfait mais conforme à nos attentes (les miennes en tout cas) ! +# Soit dit en passant, la première version de l'algorithme +# laissait déjà le dernier noeud inchangé. +# +# Un peu d'aléatoire en plus +# ========================== +# +# La solution n'est pas parfaite en ce sens que visuellement, on voit que certaines +# parties du chemin pourraient être facilement améliorées. Mais si la solution +# était parfaite en toute circonstance, nous aurions +# trouvé un algorithme à temps polynômial ce qui est +# impossible. Dans notre cas, l'algorithme produit toujours la même +# solution car il parcourt les noeuds toujours dans le même sens. +# Un peu d'aléa devrait l'aider à trouver de meilleures solutions après quelques essais. + +# In[8]: + + +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + + +def permutation_rnd(x, y, ordre): + d = longueur(x, y, ordre) + d0 = d + 1 + it = 1 + while d < d0: + it += 1 + print("iteration", it, "d=", d, "ordre[0]", ordre[0]) + d0 = d + for i in range(1, len(ordre) - 1): + for j in range(i + 2, len(ordre) + 1): + ik = random.randint(1, len(ordre) - 1) + il = random.randint(ik + 1, len(ordre)) + r = ordre[ik:il].copy() + r.reverse() + ordre2 = ordre[:ik] + r + ordre[il:] + t = longueur(x, y, ordre2) + if t < d: + d = t + ordre = ordre2 + return ordre + + +ordre = permutation_rnd(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") +plt.text(xo[0], yo[0], "0", color="r", weight="bold", size="x-large") +plt.text(xo[-2], yo[-2], "N-1", color="r", weight="bold", size="x-large") + +################################# +# Ca a l'air de marcher un peu mieux mais quelques aberrations car +# l'aléatoire n'est pas un parcours systématique de toutes les pairs. +# Par conséquent, il peut rester des croisements : + + +ordre = permutation_rnd(x, y, list(range(len(x)))) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") +plt.text(xo[0], yo[0], "0", color="r", weight="bold", size="x-large") +plt.text(xo[-2], yo[-2], "N-1", color="r", weight="bold", size="x-large") + + +###################################### +# Pour éviter cela, on peut imposer un nombre d'itérations minimum +# et recommencer plusieurs à partir d'ordre initiaux aléatoires : + + +def permutation_rnd(x, y, ordre, miniter): + d = longueur(x, y, ordre) + d0 = d + 1 + it = 1 + while d < d0 or it < miniter: + it += 1 + d0 = d + for i in range(1, len(ordre) - 1): + for j in range(i + 2, len(ordre) + 1): + ik = random.randint(1, len(ordre) - 1) + il = random.randint(ik + 1, len(ordre)) + r = ordre[ik:il].copy() + r.reverse() + ordre2 = ordre[:ik] + r + ordre[il:] + t = longueur(x, y, ordre2) + if t < d: + d = t + ordre = ordre2 + return ordre + + +def n_permutation(x, y, miniter): + ordre = list(range(len(x))) + bordre = ordre.copy() + d0 = longueur(x, y, ordre) + for i in range(0, 20): + print("iteration", i, "d=", d0) + random.shuffle(ordre) + ordre = permutation_rnd(x, y, ordre, 20) + d = longueur(x, y, ordre) + if d < d0: + d0 = d + bordre = ordre.copy() + return bordre + + +################################## +# La distance initiale. +ordre = list(range(len(x))) +print("longueur initiale", longueur(x, y, ordre)) + +################################## +# La longueur obtenue. + +ordre = n_permutation(x, y, 20) +print("longueur min", longueur(x, y, ordre)) +xo = [x[o] for o in ordre + [ordre[0]]] +yo = [y[o] for o in ordre + [ordre[0]]] +plt.plot(xo, yo, "o-") +plt.text(xo[0], yo[0], "0", color="r", weight="bold", size="x-large") +plt.text(xo[-2], yo[-2], "N-1", color="r", weight="bold", size="x-large") + + +# C'est mieux. diff --git a/_doc/practice/algocompose.rst b/_doc/practice/algocompose.rst index 1af19c65..3b2d9ed8 100644 --- a/_doc/practice/algocompose.rst +++ b/_doc/practice/algocompose.rst @@ -10,3 +10,5 @@ se construisent sous la forme d'un assemblage d'algorithme plus simple. :caption: A parcourir algo-base/exercice_morse + ../auto_examples/plot_tsp + ../auto_examples/plot_einstein_riddle diff --git a/_doc/practice/index.rst b/_doc/practice/index.rst index 92d23bd0..ade03aad 100644 --- a/_doc/practice/index.rst +++ b/_doc/practice/index.rst @@ -25,7 +25,7 @@ La seconde série illustre des algorithmes à connaître. algo101 algocompose -La troisième série sert à vous évaluer. +La dernière série sert à vous évaluer. .. toctree:: :maxdepth: 1