Lycée Buffon MP*/PSI 014-15 Épreuve d informatique du concours blanc, jeudi 5 mars 015 (3h00) Les documents, téléphones portables, ordinateurs et calculatrices sont interdits. Le sujet de cette épreuve est la résolution du problème classique du voyageur de commerce. dont l énoncé est le suivant (source Wikipédia) : étant donné n points (des «villes») et les distances séparant chaque couple de points, trouver un chemin de longueur totale minimale qui passe exactement une fois par chaque point et revienne au point de départ. Ce problème est plus compliqué qu il n y paraît : on ne connaît pas de méthode de résolution permettant d obtenir des solutions exactes en un temps raisonnable pour de grandes instances (grand nombre de villes) du problème. Pour ces grandes instances, on devra donc souvent se contenter de solutions approchées, car on se retrouve face à une explosion combinatoire : le nombre de chemins possibles passant par 71 villes est déjà un nombre d une longueur de 100 chiffres, sachant qu un nombre d une longueur de 80 chiffres permettrait déjà de représenter le nombre d atomes dans tout l univers connu. Ce problème peut servir tel quel à l optimisation de trajectoires de machines-outils : par exemple, pour minimiser le temps total que met une fraiseuse à commande numérique pour percer n points dans une plaque de tôle. Il se posait également pour optimiser la vitesse de tracé d une structure (en BTP, par exemple) par une table traçante à l époque où ces périphériques étaient mécaniques, et non électroniques comme aujourd hui. Plus généralement, divers problèmes de recherche opérationnelle se ramènent au voyageur de commerce. Dans une première partie on met en place les procédures qui permettent de lire les différents jeux de données dans une base de données pré-existante. Dans une deuxième partie, on explore un algorithme heuristique appelé algorithme par colonies de fourmis. Dans une troisième partie, on se pose la question de la résolution exacte et de sa complexité. N.B. Tous les algorithmes devront être implémentés directement en langage Python. Il y a un descriptif rapide de certaines fonctions en appendice. 1 Récupération des données Le réseau de n villes, numérotées de 0 à n 1, est modélisé par un graphe (ensemble d arètes reliant des villes) qui sera supposé ici non orienté. C est-à-dire qu on considère que si l on peut aller d une ville i à une autre ville j, alors le chemin inverse est possible avec le même coût, qui sera un nombre réel positif d i, j = d j,i représentant par exemple la distance entre ces villes. Il est possible qu il n y ait pas d arête reliant deux villes données, ce qui sera, suivant les conventions des jeux de données de provenance différentes, représenté par un coût nul ou négatif. Ainsi la donnée d un tel réseau est la donnée d une matrice n n symétrique (d i, j ) 0 i, j<n, ayant des nombres réels négatifs ou nuls sur la diagonale (car on exclut les arêtes joignant une ville à elle même) et des nombres réels ailleurs. Comme elle est symétrique, on ne stocke que les valeurs situés sous la diagonale. Pour des raisons indépendantes de notre volonté, la convention a été prise de stocker aussi les valeurs de la diagonale et donc de représenter cette matrice par une liste de n(n+1) réels d = [d 0,0,d 1,0,d 1,1,...,d i,0,d i,1,...,d i,i,...,d n 1,0,...,d n 1,n 1 ] Suivant la provenance du jeu de données, il arrive que ces nombres soient stockés directement dans un fichier texte, en étant séparés par un nombre quelconque d espaces et de sauts de lignes, à l exception de tout autre caractère. 1. Écrire une fonction lirelistenb(nomfic) qui prend en argument une chaîne de caractères nomfic représentant le chemin d accès complet d un tel fichier et retourne la liste d des réels dans l ordre du fichier. Il arrive aussi que soient stockées, non pas les distances, mais les coordonnées des villes, soit dans un système euclidien (mode eucl ) de deux coordonnées (cas des régions planes ou petites qu on approche bien par un plan), soit dans d autres systèmes de coordonnées. Le seul autre système de coordonnées qu on considèrera ici est le système géographique global longitude, latitude (mode geog ), dans lequel un point est repéré par sa longitude (un réel variant, d ouest en est, de 180 à 180 représentant un angle en degrés) et sa latitude (un réel variant, du sud au nord, de 90 à 90 représentant un angle en degrés).. Écrire une fonction distance(m,n,systcoord) qui prend en arguments deux listes de réels m et n représentant les coordonnées de deux points (M et N) et une chaîne de caractères systcoord représentant le système de coordonnées qui est soit eucl, soit geog, et retourne la distance entre les deux points considérés. On fera l approximation que la terre est une sphère de diamètre 174km.
Dans ce cas, les coordonnées sont stockées dans un fichier texte qui contient sur chaque ligne les coordonnées d une ville, séparées uniquement par des espaces, dans l ordre des villes de 0 à n 1. 3. Écrire une fonction lirelistecoord(nomfic,systcoord) qui prend en arguments une chaîne de caractères nomfic représentant le chemin d accès complet du fichier et une chaîne de caractères systcoord représentant le système de coordonnées qui est soit eucl, soit geog, et retourne la liste des distances d i, j entre les villes dans l ordre défini plus haut. Une fois la liste des distances d obtenue, il est plus aisé de travailler avec un tableau bidimensionnel représentant les distances par lignes : mat = [[d 0,0 ],[d 1,0,d 1,1 ],...,[d i,0,d i,1,...,d i,i ],...,[d n 1,0,...,d n 1,n 1 ]] 4. Écrire une fonction recupere_n(nnplusunsur) qui prend en argument un entier nnplusunsur, et qui retourne la valeur None si cet entier n est pas de la forme n(n+1) et la valeur entière n si cet entier est de la forme n(n+1), sans utiliser de boucle (for ou while). 5. Écrire une fonction creematricetriang(listenb) qui prend en argument une liste des distances listenb avec les conventions précédentes et retourne la liste de listes mat correspondante avec la convention ci-dessus. On veut maintenant se débarasser des valeurs diagonales inutiles. Par ailleurs, pour des raisons pratiques d unification, il est utile de pouvoir disposer de distances entre toutes les villes, même celles non reliées initialement, pour lesquelles on mettra alors une distance assez grande pour éviter tout chemin minimal de prendre l arête correspondante. Cette distance infinie d est fixée à la somme de toutes les distances positives (i.e. celles des arêtes existant réellement) plus un. On nomme cet opération le formatage de la matrice. Le résultat en est une liste de listes res = [[],[ d 1,0 ],...,[ d i,0,..., d i,i 1 ],...,[ d n 1,0,..., d n 1,n ]] où d i, j = d i, j si d i, j > 0 et d i, j = d sinon, qu on appelle dans la suite matrice formatée. 6. Écrire une fonction formate(mat) qui prend en argument une liste de listes mat vérifiant les conventions des questions précédentes, ne modifie pas la liste mat et retourne trois objets dans l ordre : la liste formatée res qui ne contient plus les valeurs diagonales et dont les valeurs négatives ont été remplacées par la valeur d, le nombre n de villes et l entier Python infini ayant pour valeur d. Tous ces jeux de données sont répertoriés dans une base de données contenant une seule relation (table) nommée TSP (comme Travelling Salesperson Problem) dont les attributs sont le nom Name, le nombre de villes n, le mode de stockage stomode ( mat ou coord ), s il y a lieu le système de coordonnées coordsyst ( eucl, geog ou none ), et le nom relatif (i.e. sans le chemin du répertoire) filename du fichier où sont stockées les données. Elle se présente donc ainsi : TSP Name n stomode coordsyst filename gr17 17 mat none gr17.tsp gr4 17 mat none gr4.tsp galact 4 coord geog geo4.tsp BBT 43 coord eucl eucl43.tsp On admet qu on dispose de deux fonctions, l une interrogebase(str), qui permet d interroger la base de données à l aide d une requête SQL exprimée dans la chaîne de caractères str, retournant la table résultat sous forme d une liste de listes en Python, et l autre chemintsp() qui retourne le chemin du répertoire dans lequel sont stockés les fichiers des jeux de données sur la machine locale (par exemple ce pourrait être /home/fred/ipt/devoirs/tsp/data/ ). 7. Écrire une fonction creematriceformatee(nom) qui prend en argument le nom d un jeu de données sous forme de chaîne de caractère et retourne la matrice formatée correspondante. Il sera utile dans la suite, étant donnée une matrice formatée, d avoir une fonction distance qui évite les tests récurrents sur l ordre des indices losqu on veut récupérer la distance entre deux villes. 8. Écrire une fonction dist(i,j,mat) qui prend en arguments deux numéros de villes i et j et la matrice formatee mat et retourne la distance entre les deux villes quel que soit l ordre entre i et j. Il sera aussi utile d avoir une fonction calculant la longueur d un chemin, fermé ou non. 9. Écrire une fonction longueur(chemin,mat) qui prend en arguments un liste de numéros de villes chemin et une matrice formatée mat et qui retourne la longueur du chemin correspondant calculée sur la base des distances de la matrice mat. Par exemple, si chemin=[,5,8,3], alors la longueur sera la somme des distances d 5, + d 8,5 + d 8,3.
Colonies de fourmis Le premier algorithme de colonies de fourmis à avoir été utilisé est appelé le Ant system (système fourmi). Il visait notamment à résoudre le problème du voyageur de commerce, ce qu on explique ici. L algorithme général est relativement simple, et repose sur un ensemble de fourmis, chacune parcourant un trajet parmi ceux possibles. À chaque étape, la fourmi choisit de passer d une ville à une autre en fonction de quelques règles : elle ne peut visiter qu une fois chaque ville ; plus une ville est loin, moins elle a de chance d être choisie (c est la «visibilité») ; plus l intensité de la piste de phéromone disposée sur l arête entre deux villes est grande, plus le trajet aura de chance d être choisi ; une fois son trajet terminé, la fourmi dépose, sur l ensemble des arêtes parcourues, plus de phéromones si le trajet est court ; les pistes de phéromones s évaporent progressivement à chaque itération. Pour implémenter cet algorithme, il est d abord nécessaire de créer des objets fourmis. Chaque objet de ce type devra être capable de stocker l information du chemin déjà parcouru que nous appellerons le passé et de la liste des villes restant à visiter, que nous appelleront le futur. 10. Définir une classe fourmi d objets ayant une seule méthode, le constructeur et deux attributs (champs) : une liste passe stockant, dans l ordre de visite, toutes les villes déjà visitées, la première étant la ville de départ et la dernière la position actuelle ; une liste (non forcément ordonnée) des villes restant à visiter, la ville de retour (qui est la ville initiale) étant exclue. Lors de la création on passera (outre l argument self habituel) deux arguments n (nombre de villes) et k (la ville de départ). Le passé vaudra alors [k] et le futur sera une liste contenant une fois et une seule chaque entier compris entre 0 compris et n 1, à l exception de k. Par exemple, les commandes Alphonsine = fourmi(6,) print(alphonsine.passe,alphonsine.futur) donnent le résultat suivant : [],[0,1,3,4,5] l ordre de la deuxième liste pouvant éventuellement être différent. Remarquer que le constructeur peut aussi servir à la réinitialisation d un objet fourmi f déjà existant avec la syntaxe f. init (n,k) On dépose au départ un même certain nombre nbfpv de fourmis dans chaque ville. Une itération du processus global consiste à faire parcourir à chaque fourmi un tour complet des villes avec retour dans la ville initiale. Les déplacements effectués à l intérieur de chaque itération sont régis par la règle suivante, appelée «règle aléatoire de transition proportionnelle», qui donne les probabilités pour le choix de la prochaine ville où ira la fourmi : j J, p i, j (t) = τ i, j(t) a η b i, j l J τ i,l (t) a η b, i,l où i est la position présente et J est le futur de la fourmi, η i, j est la visibilité, qui est égale à l inverse de la distance entre les deux villes i et j (1/d i, j ) et τ i, j (t) est l intensité de phéromones sur l arête (i, j) pendant l itération présente t. Les deux principaux paramètres contrôlant l algorithme sont a et b, qui contrôlent l importance relative de l intensité et de la visibilité d une arête. Pour éviter de diviser par zéro, on maintient toujours une certaine quantité de phéromones τ min sur chaque arête. À la fin de chaque itération t de l algorithme, les phéromones déposées aux itérations précédentes par les fourmis s évaporent de (1 r)τ i, j (t) (r paramètre à régler). Une fois la tournée des villes effectuée, une fourmi k dépose une quantité τ k i, j (t) = Q L de phéromone sur chaque arête (i, j) de son parcours, où L est la longueur totale du parcours et Q est un paramètre à régler, qu on exprime en pratique sous la forme q d. Il reste alors sur chaque arête la somme des phéromones qui ne se sont pas évaporées et de celles qui viennent d être déposées : τ i, j (t + 1) = rτ i, j (t) + k fourmi τ k i j(t). On veut alors construire une fonction tspfourmi(mat,a=1,b=1,taumin=1,q=0.,r=0.9,nbfpv=10,tmax=100) implémentant cet algorithme avec un nombre tmax d itérations. Pour ne pas avoir à passer trop d arguments aux fonctions auxiliaires, on choisit de créer des fonctions locales à l intérieur de la fonction tspfourmi, ce qui donne : 3
def tspfourmi(mat,a=1,b=1,taumin=1,q=0.,r=0.9,nbfpv=10,tmax=100): matloc,n,infini = formate(mat) tau = [[taumin for j in range(n)] for i in range(n)] def calculpoids(f,poids): # Probas de transition d une fourmi import random,itertools,bisect def tirage(poids): # Tire au hasard l indice de la prochaine ville # dans la liste du futur d une fourmi donnee cumul = list(itertools.accumulate(poids)) x = random.random() * cumul[-1] return bisect.bisect(cumul,x) def deplacement(i): # Effectue le deplacement poids = [0.0]*i for f in popfourmis: calculpoids(f,poids) f.passe.append(f.futur.pop(tirage(poids))) def findetour(): # Effectue toutes les operations de fin d iteration # Tour optimal tourlongmin = [[],infini] # Creation des fourmis popfourmis = [] for k in range(n): popfourmis += [fourmi(n,k) for i in range(nbfpv)] # Boucle principale for t in range(tmax): for i in range(n-1,0,-1): deplacement(i) findetour() return tourlongmin 11. Expliquer en quelques mots le fonctionnement de la fonction deplacement et dire en particulier pourquoi elle est appelée dans le bloc principal de la fonction par la boucle for i in range(n-1,0,-1): deplacement(i) 1. Écrire la fonction calculpoids(f,poids) qui prend en arguments une fourmi f et une liste de réels poids, de même taille que la liste f.futur, qui ne retourne rien mais met dans la liste poids les probabilités de transition correspondant aux villes de la liste f.futur (qui sont les villes visitables par f), avec l ordre correspondant. 13. Écrire la fonction findetour() sans arguments et ne retournant rien mais terminant le tour et mettant à jour les valeurs des phéromones et la liste à deux éléments tourlongmin, dont le premier élément est le chemin le plus court trouvé et le deuxième est sa longueur. 4
3 Résolution exacte 14. Compléter la fonction permut() pour que l appel permutations(liste) affiche à l écran toutes les permutations possibles la liste liste, sans toutes les stocker. import copy def permutations(liste): def permut(): copie = copy.deepcopy(liste) permutation = [] permut() 15. Modifier cette fonction pour obtenir une fonction tsppermut(mat) prenant en argument une matrice formatée et retournant le tour le plus court ainsi que sa longueur. 16. Montrer que la complexité de la fonction précédente n est pas polynômiale. Comparer cette complexité aux fonctions exponentielles. Quelques fonctions Ouverture d un fichier en lecture : f=open(nomfic, r ) Fermeture du fichier : f.close() Retourne l ensemble du fichier dans une unique chaîne de caractères (saut de lignes compris) : f.read() Découpe une chaîne de caractères str en une liste de mots séparés par des groupes de caractères espaces, en ne gardant aucun de ces séparateurs : str.split() ; par exemple 1 3.split() retourne [ 1,, 3 ] Découpe une chaîne de caractères str en une liste de lignes séparés par des caractères de fin de lignes, en ne gardant pas ces séparateurs : str.splitlines() Accessibles dans le module math : math.cos(), math.sin(), math.pi, math.acos(), math.sqrt() Ajoute l élément x à la fin de la liste liste : liste.append(x) Insère l élément x à la place i de la liste liste : liste.insert(i,x) Enlève l élément à la place i de liste liste en le retournant : liste.pop(i) ; s il n y a pas d argument cela enlève et retourne le dernier élément de la liste. 5