UNIVERSITE PARIS VII - DENIS DIDEROT U.F.R. D'INFORMATIQUE THESE
|
|
|
- Jean-Philippe St-Denis
- il y a 10 ans
- Total affichages :
Transcription
1 UNIVERSITE PARIS VII - DENIS DIDEROT U.F.R. D'INFORMATIQUE Année 1998 N attribué par la bibliothèque _ THESE pour obtenir le grade de DOCTEUR DE L'UNIVERSITE PARIS VII Discipline : Informatique présentée et soutenue publiquement par François Laburthe le Titre : Contraintes et Algorithmes en Optimisation Combinatoire Directeur de thèse : Yves Caseau M. Nivat : président JURY
2 J.-L. Lambert : rapporteur C. Roucairol : rapporteur Y. Caseau : directeur de thèse F. Fages : examinateur E. Jacquet Lagrèze : examinateur P. Van Hentenryck : examinateur 2
3 3
4 Table des matières 0. Introduction Sujet...Erreur! Signet non défini. 0.2 Motivations...Erreur! Signet non défini. 0.3 Méthode...Erreur! Signet non défini. 0.4 Historique...Erreur! Signet non défini. 0.5 Plan de l étude...erreur! Signet non défini. 1. Chapitre 1. Formalismes pour l optimisation combinatoire...erreur! Signet non défini. 1.1 Programmation linéaire...erreur! Signet non défini. 1.2 Programmation par contraintes...erreur! Signet non défini. 1.3 Recherche arborescente globale...erreur! Signet non défini. 1.4 Algorithmes approchés, recherche locale...erreur! Signet non défini. 1.5 Un exemple : la couverture d ensembles...erreur! Signet non défini. 2. Chapitre 2. Couplages et allocation...erreur! Signet non défini. 2.1 Les problèmes de couplages bipartis...erreur! Signet non défini. 2.2 Couplages bipartis et flots...erreur! Signet non défini. 2.3 Modélisation linéaire, méthode Hongroise...Erreur! Signet non défini. 2.4 Programmation par contraintes...erreur! Signet non défini. 2.5 Variations...Erreur! Signet non défini. 2.6 Bilan...Erreur! Signet non défini. 3. Chapitre 3. Ordonnancement disjonctif...erreur! Signet non défini. 3.1 Ordonnancement...Erreur! Signet non défini. 3.2 Contraintes...Erreur! Signet non défini. 3.3 Techniques de Recherche Opérationnelle...Erreur! Signet non défini. 3.4 Solution heuristique et optimisation locale...erreur! Signet non défini. 3.5 Bilan...Erreur! Signet non défini. 4. Chapitre 4. Le voyageur de commerce (TSP)...Erreur! Signet non défini. 4.1 Circuits Hamiltoniens...Erreur! Signet non défini. 4.2 Algorithmes de résolution exacte...erreur! Signet non défini. 4
5 4.3 Contraintes...Erreur! Signet non défini. 4.4 Solutions heuristiques...erreur! Signet non défini. 4.5 Optimisation locale...erreur! Signet non défini. 4.6 Bilan...Erreur! Signet non défini. 5. Chapitre 5. Problèmes de grilles d emplois du temps...erreur! Signet non défini. 5.1 Introduction, typologie...erreur! Signet non défini. 5.2 Un problème académique : l'allocation matricielle...erreur! Signet non défini. 5.3 Contraintes redondantes...erreur! Signet non défini. 5.4 Application à des problèmes réels d'emplois du temps....erreur! Signet non défini. 5.5 Conclusion...Erreur! Signet non défini. 6. Chapitre 6. Ordonnancement cumulatif...erreur! Signet non défini. 6.1 Ordonnancement et ressources...erreur! Signet non défini. 6.2 Techniques de propagation de contraintes...erreur! Signet non défini. 6.3 Stratégies d énumération...erreur! Signet non défini. 6.4 Expériences et résultats...erreur! Signet non défini. 6.5 Autres techniques...erreur! Signet non défini. 6.6 Bilan...Erreur! Signet non défini. 7. Chapitre 7. Problèmes de tournées...erreur! Signet non défini. 7.1 Tournées de véhicules...erreur! Signet non défini. 7.2 Algorithmes de résolution...erreur! Signet non défini. 7.3 Insertion et optimisation locale incrémentale...erreur! Signet non défini. 7.4 Cas du VRPTW...Erreur! Signet non défini. 7.5 Application à des applications réelles de tournées...erreur! Signet non défini. 7.6 Bilan...Erreur! Signet non défini. 8. Chapitre 8. Bilan...Erreur! Signet non défini. 8.1 Cartographie de la résolution de problèmes combinatoires...erreur! Signet non défini. 8.2 P.P.C. et Optimisation Combinatoire...Erreur! Signet non défini. 8.3 Algorithmes hybrides...erreur! Signet non défini. 8.4 Perspectives...Erreur! Signet non défini. 9. Chapitre 9. Un langage pour les algorithmes hybrides : programme de recherches....erreur! Signet non défini. 5
6 9.1 Un langage de modélisation et de simulation...erreur! Signet non défini. 9.2 Structures de données algorithmiques additionnelles...erreur! Signet non défini. 9.3 Description déclarative de comportements...erreur! Signet non défini. 9.4 contrôle...erreur! Signet non défini. 9.5 Environnement...Erreur! Signet non défini. 9.6 Conclusion...Erreur! Signet non défini. 10. Chapitre 10. Définition du langage SaLSA...Erreur! Signet non défini Pourquoi et comment décrire le flot de contrôle...erreur! Signet non défini Décisions locales...erreur! Signet non défini Composition des choix...erreur! Signet non défini Grammaire du langage SaLSA...Erreur! Signet non défini. 11. Chapitre 11. Utilisation de SaLSA pour résoudre des problèmes combinatoires...erreur! Signet non défini Stratégies de recherche globale pour un résolveur de contraintes sur les domaines finis....erreur! Signet non défini GSAT...Erreur! Signet non défini Algorithmes d insertion pour des problèmes de tournées...erreur! Signet non défini Heuristiques en ordonnancement....erreur! Signet non défini Méthodes complètes en ordonnancement et shaving...erreur! Signet non défini Optimisation locale et solutions partielles : «Shuffling»...Erreur! Signet non défini Jeux d échecs...erreur! Signet non défini Recherche de bonnes cliques maximales en ordonnancement cumulatif...erreur! Signet non défini La procédure d optimisation locale de Lin & Kernighan pour le TSPErreur! Signet non défini. 12. Chapitre 12. Sémantique opérationnelle et implémentation...erreur! Signet non défini Préliminaires et notations...erreur! Signet non défini Transitions du calcul de processus...erreur! Signet non défini Implémentation du générateur de code...erreur! Signet non défini. 13. Conclusion...Erreur! Signet non défini. 14. Bibliographie...Erreur! Signet non défini. 6
7 7
8 Remerciements Je remercie Maurice Nivat de me faire l'honneur de présider ce jury, témoignant ici de son intérêt pour mes travaux. Je souhaite remercier très particulièrement Yves Caseau pour le plaisir que j'ai eu à travailler avec lui, pour sa vision scientifique et ses conseils avisés, pour sa direction efficace et confiante et enfin, pour l'attention et la disponibilité dont il a fait preuve. Je remercie François Fages et Guy Cousineau de m'avoir chaleureusement accueilli au LIENS. Mon intégration à l'équipe Langages Logiques, Contraintes et Optimisation m'a permis d'avoir un cadre bien agréable pour mener cette thèse, et la collaboration amicale avec l'équipe a été fructueuse. Je remercie Jean Jourdan et Béatrice Bacconnet de m'avoir accueilli au Laboratoire Central de Recherches de Thomson-CSF. J'ai beaucoup apprécié notre collaboration et la confiance qu'ils m'ont accordée en décidant d'utiliser Claire, Eclair et SaLSA. Je remercie particulièrement Éric Jacopin, Jean-Luc Lambert et Catherine Roucairol qui ont relu ce mémoire avec une minutie bienveillante, pour leurs suggestions pertinentes. Je remercie Krysztof Apt et Monique Laurent pour leur invitation à venir présenter mes travaux aux CWI. L'intérêt qu'ils leur ont porté est une reconnaissance chaleureuse. Je remercie Eric Jacquet-Lagrèze et Pascal Van Hentenryck d'avoir accepté de prendre de leur temps pour participer à ce jury. Je remercie enfin tous ceux avec lesquels j'ai eu le plaisir de reflechir au tableau ou devant une feuille blanche. Ces moments d'échange comptent parmi les meilleurs souvenirs de ces quatre années. Pour ces passionantes discussions, pour le plaisir de chercher et argumenter ensemble, je souhaiterais remercier Philippe Baptiste, Hervé Chibois, Simon De Givry, Akim D le, Sébastien Desreux, Francois Fages, Éric Jacopin, Tibor Kökény, Claude Le Pape, Domitile Lourdeaux, Laurent Perron, Pierre Savéant, Vincent Schächter et Mark Wallace. Enfin, merci à celui, qui m'a montré, il y a maintenant longtemps, en faisant dessiner une maison à une petite tortue, que l'informatique, c'est cool. 8
9 9
10 0. Introduction 0.1 Sujet Le but de cette thèse est d étudier les apports mutuels de la programmation par contraintes et de la recherche opérationnelle à résolution de problèmes d optimisation combinatoire réels. Cet objectif se décline selon deux axes : d une part trouver les techniques algorithmiques pertinentes pour la résolution de tels problèmes et en particulier celles relatives à la programmation par contraintes, et d autre part, les mettre en oeuvre dans un environnement informatique. La première direction de recherche veut répondre à la question «Que faut-il faire pour résoudre de tels problèmes combinatoires?», alors que la seconde vise la question «Comment faut-il le faire?». 0.2 Motivations Quand ce travail de recherche a commencé, les réponses à ces questions étaient mal connues, pressenties ou dispersées. Un certain nombre de techniques algorithmiques (i.e. la programmation linéaire, la programmation par contraintes, les algorithmes de graphes, la recherche tabou, etc.) étaient étudiées dans des communautés séparées. Chacune avait obtenu de bons résultats ou des résultats prometteurs sur certains problèmes. En particulier, la programmation par contraintes effectuait ses premières percées en milieu industriel, provoquant des réactions contrastées : réactions enthousiastes devant la flexibilité de son formalisme qui permet de décrire des problèmes fort complexes, prenant en compte de nombreux aspects spécifiques à une application; réactions sceptiques devant la piètre qualité des résultats obtenus par rapport à d autres techniques algorithmiques, lors de mises en œuvre naïves. Face à l'attrait de cette nouvelle technique qu'était la programmation par contraintes, la littérature était de peu d'aide à celui qui voulait résoudre un problème d'optimisation particulier. En effet, peu de comparaisons systématiques entre ces techniques algorithmiques étaient disponibles, et si un certain nombre de chercheurs savaient de manière expérimentale, pour un problème donné, quelles étaient les techniques pertinentes à utiliser, cette connaissance dispersée n'était pas accessible dans les publications. Enfin, une fois identifiés les algorithmes pertinents à la résolution du problème, l'expérimentation informatique pouvait s'avérer problématique. En effet, chacune des techniques étant décrite dans son formalisme propre, l'utilisateur, à moins d'investir beaucoup de temps en programmation, en était généralement réduit à l'utilisation d'un outil spécifique (système CLP, résolveur linéaire, bibliothèque d'algorithmes de graphe). Ces outils relativement rigides limitaient le brassage des algorithmes et l'expérimentation de combinaisons des méthodes de résolution. Cette situation nous a incité à proposer à la communauté une évaluation de la programmation par contraintes face aux autres grandes techniques algorithmiques, sur un ensemble de problèmes d'optimisation classiques, pour mettre à la disposition de celui qui envisage la résolution d'un problème similaire, une synthèse des techniques appropriées, directement utilisable. De plus, nous avons voulu apporter notre contribution aux problèmes de génie logiciel des algorithmes hybrides d'optimisation en proposant un langage pour la programmation d'algorithmes de recherche locale et globale. 0.3 Méthode La méthode suivie a consisté à répondre d'abord à la première question avant d'aborder la seconde. En effet, de manière pragmatique, nous voulions d'abord délimiter les algorithmes avant de proposer un langage pour les exprimer. Pour répondre à la première question, nous avons décidé de faire un tour d horizon des problèmes d optimisation combinatoire. On s est limité à un ensemble de problèmes qui semblaient revenir de 9
11 manière récurrente dans le contexte d applications réelles, comme l'ordonnancement, l'affectation de ressources, l'optimisation de parcours et les problèmes de grilles d'emplois du temps. Pour chacun de ces problèmes, nous avons voulu comparer les techniques algorithmiques classiques (du moins, celles programmables en un temps raisonnable), avec des techniques de programmation par contraintes, d un double point de vue : = celui de l efficacité. On veut comparer des algorithmes suivant leur rapidité et suivant la taille maximale des problèmes résolus. = celui de la robustesse. On veut évaluer, pour chaque algorithme, la capacité à résoudre des problèmes voisins, c est à dire des problèmes dont on a légèrement changé les spécifications de départ, en ajoutant certaines contraintes additionnelles spécifiques, ou en introduisant des dépendances nouvelles entre certaines parties du problème. Le premier objectif était d obtenir une cartographie pour ces problèmes d optimisation combinatoire qui mette en relation des zones d utilisation (taille du problème et présence ou non de contraintes additionnelles) avec des techniques algorithmiques particulières. En particulier, on cherche à évaluer les limites d une résolution en programmation par contraintes (et le cas échéant, à les repousser). Notons que ce premier objectif ne constitue pas exactement une évaluation de la programmation par contraintes pour la résolution de problèmes combinatoires réels. Notre méthode a introduit un biais en délaissant les problèmes vraiment difficiles à modéliser (souvent trop spécifiques pour être présenter un intérêt général) et en se concentrant sur de vrais problèmes combinatoires (sur lesquels on peut mener des comparaisons de manière rigoureuse). En pratique, il semble qu'il existe de nombreux problèmes où la difficulté réside bien plus dans la modélisation que dans la résolution, et qu'une approche naïve par contraintes parvient à résoudre de manière satisfaisante (quand ces problèmes sont soit petits, soit denses en solutions). Mais une telle affirmation étant difficile à étayer de manière scientifique, nous avons laissé de telles situations en dehors du champ d'expériences de ce travail. Le second objectif - fournir un environnement pour le développement des algorithmes pertinents - a été abordé au tout début de ce travail, puis plus tard dans la thèse. La première étape a consisté à proposer un langage de programmation simple et élégant, CLAIRE [CL 96a], dans lequel nous avons programmé tous les algorithmes décrits dans ce travail. Cette expérience de programmation ainsi que l'interaction avec d'autres utilisateurs de CLAIRE nous a permis d'aborder à nouveau la question du langage plus tard dans la thèse. Parmi les nombreuses possibilités ouvertes, nous aurions pu, par exemple, proposer une extension du cadre CLP, ou un système d'optimisation intégrant un certain nombre d'objets prédéfinis (des tâches, des ressources, des grilles, des réseaux, etc. ), ou encore un système avec des contraintes sur les relations dans la lignée d'alice [Lau 78]. En 1996, nous ne nous sommes pas senti assez mûrs pour franchir ce pas, et nous n'avons pas voulu proposer un système fermé proposant un ensemble non-extensible par l'utilisateur de mécanismes d'optimisation. Nous avons préféré nous concentrer sur une difficulté : l'expression du contrôle pour certains algorithmes hybrides. Nous avons proposé un langage, SaLSA, qui permet de décrire de manière élégante et très synthétique des algorithmes hybrides complexes et nous avons implémenté un compilateur de SaLSA vers CLAIRE. Cette approche de génération de code (ou compilation) a l'intérêt de pouvoir s'appliquer à n'importe quel langage cible de programmation et de ne pas se limiter à un système particulier. Pour proposer le langage SaLSA, notre démarche a été pragmatique et a commencé par réunir un cahier des charges rassemblant tous les algorithmes complexes de recherche arborescente que nous avions rencontrés. Le langage à proposer devait ainsi permettre de les exprimer tous de manière simple. Après avoir proposé et implémenté la première version de SaLSA, nous nous sommes rendu compte que les opérations de base de SaLSA étaient les mêmes que celles d'un langage pour l'optimisation locale, 10
12 LOCALIZER [MVH 97]. Nous avons alors choisi d'étendre SaLSA et d'en proposer une version plus générale permettant d'exprimer aussi bien les algorithmes de recherche locale que globale. 0.4 Historique Les deux questions de la thèse s étaient posées de manière naturelle à l équipe d Yves Caseau. En effet, elle avait développé le système LAURE [Ca 91a] à Bellcore, qui proposait, au-dessus d un modèle objet issu de la représentation de connaissances, toute une série d outils pour faire de la résolution de problèmes (règles de production, règles déductives, contraintes, etc.). Ce système avait été utilisé pour réaliser des prototypes d applications industrielles en optimisation et avait, entres autres, remporté de beaux succès en enrichissant le cadre de la programmation par contraintes, soit par l utilisation d algorithmes de propagation spécialisés (en emplois du temps [CGL 92] ou en ordonnancement [CL 94]), soit par l utilisation de la propagation par contraintes dans le cadre d algorithmes plus généraux (application de LAURE à la gestion des tâches de maintenance sur un réseau téléphonique). Au cours de son utilisation, nous nous sommes rendu compte que LAURE était trop complexe et trop difficile à maîtriser pour un nouvel utilisateur. Dans les faits, une partie des technologies avancées offertes par le système n était que rarement utilisée. Le succès des applications fit apparaître un autre problème, lié à l explication d un programme à un utilisateur novice : à mesure que l'on programmait des algorithmes de plus en plus complexes, le code devenait de plus en plus difficile à comprendre, et Laure ne pouvait plus être utilisé à la fois comme langage de spécification et comme langage de programmation, comme cela avait été le cas lors d utilisations antérieures plus simples. Nous avons donc réfléchi en 1993 à la conception d un langage de spécifications formelles orientées-objets, CECILE [CL 93], qui permettrait de documenter le code LAURE. Faute de moyens, ce langage n a jamais été supporté, mais nous en avons gardé un goût fort pour les descriptions élégantes en pseudo-code de programmes complexes. A commencé alors le projet d un nouveau langage, CLAIRE [CL 96a], qui mettrait à la disposition de l utilisateur, les technologies de LAURE qui s étaient avérées utiles et dont le code serait aussi élégant et naturel que celui de CECILE. Avec un petit groupe d intéressés, nous avons passé l année à définir le langage dont Yves Caseau a réalisé la première implémentation à l été 94. A partir de ce moment, CLAIRE a été notre environnement informatique d'expérimentation. Le travail exploratoire de cartographie des méthodes de résolution par contraintes pour les problèmes d'optimisation combinatoire a été mené conjointement avec Yves Caseau, dont le cours Contraintes et Algorithmes au Magistère de l'école Normale Supérieure (MMFAI) aux printemps 95, 96 et 97 a joué le rôle d'aiguillon (il fallait pouvoir présenter dans un cours de trois heures une synthèse des techniques algorithmiques efficaces sur un problème combinatoire donné). Ce cours a aussi permis de mettre à l'épreuve le langage CLAIRE : nous avons systématiquement utilisé CLAIRE comme langage de pseudocode pour la description d'algorithmes d'optimisation qui ont pu être compris par des élèves sans formation préalable au langage. Le deuxième volet de la thèse a été abordé à partir de Après presque deux ans d'utilisation de CLAIRE et d'expérimentation en optimisation combinatoire, et d'interaction avec d'autres utilisateurs de Claire, nous pouvions commencer à considérer le pas suivant vers le système idéal de résolution de ces problèmes. Cette thématique a été explorée en liaison avec le projet ESPRIT CHIC-2 (consacré à la programmation d'algorithmes hybrides en optimisation combinatoire) auquel nous étions associés. De plus, j'ai fait ma troisième année de thèse au Laboratoire Central de Recherches de Thomson/CSF comme scientifique du contingent, où l'on m'a demandé de réfléchir à la résolution de problèmes combinatoires en temps réel avec des contraintes. Ce cadre d'utilisation rejoignait exactement nos préoccupations, puisqu'il passait par le développement d'algorithmes hybrides. Le travail lié à SaLSA a donc permis de collaborer avec de nombreuses personnes, dans le cadre du projet CHIC-2, au Laboratoire 11
13 Central de Recherches de Thomson/CSF et dans le cercle des utilisateurs de Claire (à l'ens, à Bouygues et ailleurs). Aujourd'hui, CLAIRE est utilisé dans des applications industrielles d'optimisation combinatoire à Bouygues et à Thomson-CSF et comme outil d'expérimentation dans de nombreux laboratoires universitaires (ENSAM, LIRMM, CMU,...) ainsi qu'à EDF, Lucent technologies, Axlog ingénierie, etc. 0.5 Plan de l étude Cette thèse se décompose en deux parties répondant aux deux objectifs visés : = la partie A est consacrée à l évaluation des techniques de programmation par contraintes par rapport aux techniques d algorithmique classique sur un certain nombre de problèmes d'optimisation discrète. = la partie B est consacrée à l étude de la description de tels algorithmes du point de vue du langage ou du système de programmation. On décrit maintenant, pour chaque chapitre, les principaux points abordés. Après cette introduction, le premier chapitre brosse un panorama des techniques algorithmiques disponibles pour l'optimisation combinatoire. On présente ainsi, rapidement, les principes de la programmation linéaire et de la programmation par contraintes. On montre comment ces deux techniques peuvent être utilisées dans le cadre d'algorithmes de recherche globale pour résoudre de manière exacte des problèmes d'optimisation. On présente ensuite des familles d'algorithmes approchés d optimisation, notamment les algorithmes gloutons et les méthodes de recherche locale. Enfin, on conclut en présentant la résolution d'un exemple simple, la couverture d'ensembles, en programmation linéaire et en programmation par contraintes. Partie A La partie A qui comporte sept chapitres (deux à huit), s'intéresse à un ensemble de problèmes d'optimisation. Chaque chapitre est consacré à un problème donné. Pour chacun d'entre eux, on s'efforce de présenter le problème, de motiver son étude et d'envisager les principaux algorithmes pour le résoudre. Dans ce cadre, on présente l'approche habituelle en programmation par contraintes, que l'on s'efforce d'améliorer, pour permettre une évaluation de la programmation par contraintes par rapport aux autres techniques algorithmiques sur le problème en question. Le chapitre deux présente le problème de l allocation de ressources. On commence par en décrire une formulation comme un problème de couplage maximal dans un graphe biparti et on présente dans ce cadre deux algorithmes de Recherche Opérationnelle, un algorithme de flot et un algorithme linéaire primal-dual, la méthode Hongroise. Après avoir décrit une formulation naïve par contraintes, on propose des améliorations du schéma de propagation qui permettent de résoudre des problèmes de taille moyenne (jusqu'à 2 40 noeuds), alors que la méthode Hongroise permet de résoudre des grands problèmes. On considère ensuite deux extensions du problème d'allocation pour lesquelles, contrairement au cas du problème pur, la programmation par contraintes devient compétitive avec la méthode Hongroise. On propose en conclusion l'ajout d'une nouvelle contrainte globale aux systèmes de P.P.C., dédiée aux problèmes d'affectation bijective et implémentée grâce à la méthode Hongroise. Le chapitre trois présente le problème de l ordonnancement disjonctif. Après avoir décrit une formulation linéaire, on présente un cadre complet pour sa résolution en programmation par contraintes. Celui-ci comporte des règles de propagation, basées sur l'analyse d' intervalles de tâches et un schéma de branchement, et permet une résolution exacte très compétitive des problèmes, grâce à laquelle nous avons résolu des problèmes ouverts. On aborde ensuite la famille des algorithmes approchés et on montre 12
14 comment le cadre précédent de propagation de contraintes peut être réutilisé pour proposer une heuristique gloutonne ainsi qu'une procédure d'optimisation locale très efficaces. On conclut que la propagation de contraintes est une technique essentielle à la résolution de problèmes d'ordonnancement disjonctif, tant pour leur résolution exacte que leur résolution approchée. Le chapitre quatre présente le problème du voyageur de commerce. Après avoir décrit le modèle linéaire et présenté les méthodes de résolution associées, pour le cas d'un distance symmétrique ou d'une distance asymétrique, on propose un algorithme de propagation de contraintes basé sur une formulation issue des couplages bipartis. On décrit la propagation de la contrainte d'élimination de sous-cycles, la propagation croisée avec un modèle temporel (en présence d'éventuelles fenêtres de temps). Pour les petits problèmes (moins de 15 nœuds), la PPC est ainsi un des algorithmes génériques (s'appliquant en particulier aussi bien aux distances symétriques qu'aux distances asymétriques) les plus rapides actuellement disponibles. Enfin, on complète le schéma de propagation par plusieurs techniques de bornes inférieures, basées sur des relaxations (couplage Hongrois, arbre couvrant de poids minimum et relaxation Lagrangienne à la Held & Karp). L'ensemble de ces techniques permet de résoudre, de manière exacte, des problèmes jusqu'à 70 nœuds. Pour les problèmes plus grands, on passe en revue les heuristiques gloutonnes (par insertion) ainsi que les méthodes d'optimisation locale. Le chapitre cinq s'intéresse à la résolution de problèmes d'emplois du temps. On commence par délimiter deux formulations de problèmes différents, l'allocation matricielle et la gestion de personnel. On décrit le lien entre le problème de l'allocation matricielle et d'autres problèmes combinatoires comme les multiflots, l'ordonnancement ou la reconstruction de polyominos. On présente ensuite une modélisation simple par contraintes, puis quatre techniques de propagation redondante. Pour le problème de l'allocation matricielle, on montre comment analyser de manière globale les demandes minimales et maximales sur les lignes et les colonnes et on montre comment traiter le problème limité à une ligne (ou une colonne) comme un problème de flot maximal. Pour le problème de gestion de personnel, on montre comment résoudre de manière exacte le problème où tous les individus sont semblables et où tous les horaires sont des intervalles par un algorithme de flots, et on propose une technique de borne, basée sur la méthode Hongroise, applicable au cas de gestion de personnel en présence de préférences pour les personnes. Enfin, on utilise ces techniques dans deux applications réalistes, une pour les rotations en 3/8 du personnel dans un central téléphonique, l'autre pour la planification des horaires de personnel dans un centre d'appel. Le chapitre six présente le problème de l ordonnancement cumulatif. Après avoir motivé la généralisation du cadre de l'ordonnancement disjonctif pour la modélisation de problèmes réels de production, et décrit le problème général de l'ordonnancement cumulatif et sa restriction au cadre du RCPSP, on présente les deux techniques principales de propagation de contraintes, celle issue de l'analyse des fenêtres de temps (généralisation des intervalles de tâches) et celle issue de l'analyse des demandes de ressource (prise en compte des parties fixes des tâches). On décrit ensuite un cadre de recherche arborescente basé sur la construction chronologique de l'ordonnancement et enrichi par un apprentissage des échecs et l'utilisation de règles de dominance. On applique ensuite ces techniques à trois séries de problèmes, cumulatifs monoressource, cumulatif multi-ressource et RCPSP. Enfin, on évoque d'autres techniques de propagation dont l'apport est moins clair. On conclut, comme pour l'ordonnancement disjonctif, à l'importance des techniques de contraintes, en insistant sur la variété des problèmes et des techniques utilisées. Le chapitre sept présente le problème des tournées. Après avoir évoqué rapidement les méthodes exactes impropres à traiter de grands problèmes, on décrit les principales heuristiques ainsi que les procédures d'optimisation locale, issues du problème du voyageur de commerce (TSP). On propose ensuite une nouvel algorithme basé sur une procédure d'insertion enrichie par une phase d'optimisation locale incrémentale (O.L.I) après chaque insertion. On montre comment cet algorithme fournit des résultats de bonne qualité pour un temps de calcul très rapide, pour le problèmes de tournées, avec ou sans fenêtres de temps. On propose ensuite deux améliorations de cet algorithme : l'une, fait appel à un module 13
15 d'optimisation exact pour l'optimisation de chacune des routes individuelles (petits TSP), l'autre, plonge l'algorithme d'insertion dans un arbre de recherche à branchement limités (recherche LDS ). On montre ensuite comment la complexité de cet algorithme permet de résoudre des problèmes de grande taille (2 minutes pour mille nœuds). Enfin, sur une série de petits problèmes de tournées de taille plus petite, pour lesquels on dispose de résultats comparatifs, on montre que l'on obtient des résultats compétitfs avec le meilleur algorithme de recherche tabou, et on améliore l'état de l'art sur 4 instances. Enfin, on conclut en proposant des pistes pour prendre en compte d'autres contraintes (qualification de personnel, ressources, synchronisation de tâches, etc.) dans l'algorithme d'insertion. Enfin, on propose un bilan, dans le chapitre huit, de l'ensemble de ces expérimentations avec la programmation par contraintes sur ces problèmes combinatoires. D'une part, on présente, pour chacune des familles de problèmes, une cartographie synthétique des techniques considérées. D autre part, en faisant la synthèse des algorithmes qui se sont illustrés dans cette comparaison, on présente quelques conclusions méthodologiques, évoquant en particulier l'intérêt de la combinaison de différentes techniques algorithmiques, pour aboutir à la mise en œuvre d algorithmes hybrides. Partie B La partie B s'attache à la deuxième question de la thèse, celle de la mise en œuvre d'algorithmes complexes d'optimisation dans un environnement informatique. Elle comprend quatre chapitres (neuf à douze). Comme le sujet de cette deuxième partie est vaste et que l'état de l'art est beaucoup moins avancé que pour la première partie, on ne prétend pas chercher à y répondre de manière exhaustive. On commence par faire, dans le chapitre neuf, un tour d horizon des directions de recherche qui nous ont apparu importantes au cours de ce travail, pour la mise en œuvre de procédures de résolution. On retient cinq directions majeures, sur lesquelles on indique des pistes de travail. On commence par évoquer les problèmes liés à la modélisation d'un système et d'une situation. On discute ensuite les problèmes de programmation liés à l'utilisation de structures de données spécifiques à certains algorithmes. Puis, on examine les possibilités de spécification déclarative de comportements de composants algorithmiques. On aborde ensuite la question de la programmation d'algorithmes de recherche dont le flot de contrôle est complexe. Enfin, on évoque les problèmes d'environnement de programmation qui sont spécifiques au développement d'applications en optimisation combinatoire. Parmi ces cinq points, on choisit de se concentrer sur le quatrième (la spécification du contrôle pour des algorithmes de recherche complexe) que l'on traite en détail dans les deux chapitres suivants. Le chapitre dix présente le langage SaLSA dont le but est de permettre la spécification de manière élégante et très concise des algorithmes de recherche globale, locale ou hybride. Après avoir présenté les motivations à la définition d'un tel langage, ainsi que les solutions disponibles actuellement, on présente les briques du langage, les points de choix qui correspondent à la description de décisions indéterministes. On présente ensuite une algèbre permettant de composer ces points de choix pour décrire des arbres de recherche complexes. Le chapitre onze décrit l application de SaLSA à des algorithmes présentés dans la partie A. On commence par montrer comment SaLSA permet de décrire simplement un grand nombre d'algorithmes hybrides en optimisation combinatoire. On passe en revue une dizaine de problèmes, issus, pour la plupart, des expériences de la partie A, en évoquant entre autres la spécification des algorithmes de shuffle, de GSAT, d'optimisation locale incrémentale, de recherche LDS et la procédure de Lin et Kernighan. Le chapitre douze se penche sur l'utilisation pratique du langage SaLSA. On présente une sémantique opérationnelle du langage sous forme de règles de transitions dans un calcul de processus concurrents. 14
16 Enfin, on évoque quelques aspects liés à l'implémentation, qui génère du code Claire à partir de description SaLSA. Enfin, on conclut sur cette seconde partie liée à l'étude des problèmes de mise en œuvre et de génie logiciel des algorithmes hybrides. 15
17
18 17
19 1. Chapitre 1. Formalismes pour l optimisation combinatoire L'objet de ce premier chapitre est la description des principaux formalismes pour exprimer et résoudre des problèmes d optimisation combinatoire. On commencera par présenter le cadre le plus éprouvé, celui de la programmation linéaire, puis on s'attachera au cadre qui nous intéresse particulièrement, celui de la programmation par contraintes. On décrira ensuite la partie algorithmique commune à ces deux formalismes : les algorithmes de recherche par séparation et évaluation (branch and bound). Enfin, on survolera rapidement des grandes familles d algorithmes approchés. Ce chapitre se conclura par le développement d un exemple introductif : le problème de la couverture d ensembles. 1.1 Programmation linéaire Formalisme - description des problèmes La programmation linéaire (P.L.) [Chv 83] s'attache à la résolution de problèmes qui peuvent se décrire de la façon suivante : on cherche à affecter des valeurs réelles positives à un ensemble de variables x i de sorte qu'un certain nombre d'inéquations linéaires soient satisfaites. Chaque valuation admissible (respectant toutes les inégalités) du vecteur x est appelée solution. Parmi toutes les solutions, on en cherche une qui maximise un coût dont la donnée est une fonction linéaire de x. Le problème peut ainsi s'exprimer sous la forme suivante, appelée programme linéaire : max c.x sous les contraintes A.x b, x 0 Pour que les calculs restent dans les rationnels, on impose aux contraintes (la matrice A et le membre droit b) ainsi qu'à la fonction objective (vecteur c) d'être à coefficients entiers. Il existe de multiples versions équivalentes de cette formalisation, suivant que l'on minimise ou que l'on maximise la fonction objective, et que les contraintes sont des égalités ou des inégalités. Mentionnons enfin que l'on peut imposer aux variables x i de prendre des valeurs entières : on parle alors de programmation linéaire en nombres entiers (P.L.N.E) [Sch 86]. La traduction d'un problème combinatoire dans ce formalisme n'est pas toujours évidente. Il existe généralement de nombreux encodages possibles d'un problème sous forme linéaire, et la sélection d'un bon modèle demande une certaine expertise. Pour les problèmes d'optimisation discrète qui nous intéressent, cette traduction se fait au moyen de variables entières. Plus précisément, on est souvent amené à utiliser des variables à valeur dans {0,1} pour représenter des alternatives binaires. La traduction d'un problème en formalisme linéaire génère parfois un nombre important de variables [mat 97] L algorithme du simplexe Géométriquement, l'ensemble des solutions à un programme linéaire réside dans un polyèdre de R n (où n est le nombre de variables), défini par les inégalités linéaires de A. Dans le cas où ce polyèdre est borné (et ainsi la valeur maximale du programme linéaire est finie), on parle de polytope. Savoir si le programme admet une solution revient à savoir si ce polyèdre est vide ou non. Optimiser une fonction linéaire sous ces contraintes revient à trouver le(s) sommet(s) du polyèdre qui sont les plus éloignés de l'origine suivant une direction de l'espace (celle de c). Plusieurs algorithmes génériques ont été proposés pour résoudre les programmes linéaires. L algorithme du simplexe est un algorithme géométrique élémentaire qui part d un sommet quelconque du polytope et glisse le long des arêtes vers un (le) sommet optimal. Sa complexité, dans le cas le pire, est exponentielle. Suivant [Chv 83] et [Mau 92], décrivons de manière plus précise cet algorithme. En rajoutant à chaque inéquation linéaire une variable (appelée variable d'écart), on peut transformer les 1
20 inégalités linéaires en égalités, pourvu que ces variables d'écart prennent des valeurs positives. On peut ainsi réécrire le programme linéaire sous la forme suivante : max c.x sous les contraintes A.x = b, x 0 On peut aussi transformer A pour qu'elle soit de dimension m n, où m est son rang (m n). (Notons ici que n correspond au nombre de variables de départ, augmenté du nombre de contraintes, à cause des variables d'écart). Un ensemble d'indices B = {i 1,..., i m } forme une base si les colonnes A i1,..., A im sont linéairement indépendantes. Dans ce cas, la matrice carrée A B = A i1,..., A im est inversible. On va construire une solution au système linéaire de la manière suivante : on part du vecteur x B = A -1 B b, que l'on complète en une solution complète x* en fixant à 0 toutes les autres variables x i pour i B. On a ainsi Ax* = b. La solution x* est appelée solution basique, et, dans le cas où toutes ses composantes sont positives, solution basique réalisable. Enfin, on montre que l'ensemble des sommets du polytope défini par A et b correspond exactement à l'ensemble des solutions basiques réalisables, obtenues par une base B quelconque. On montre encore que deux sommets adjacents du polytope peuvent être obtenus par deux bases identiques à un indice près. On peut ainsi passer de la description géométrique de l'algorithme du simplexe à sa description algébrique. On part d'un sommet initial sur lequel on évalue la fonction objective. Cela revient à chercher une base qui fournisse une solution basique réalisable. Ensuite, on passe d'un sommet à un sommet adjacent de sorte que la fonction objective augmente (strictement) le long de l'arête entre les deux sommets. Ce déplacement le long d'une arête correspond au remplacement, dans la base courante, d'un indice par un autre. D'autres algorithmes géométriques plus complexes ont été proposés (méthode de l ellipsoïde [Kh 79] ou du point intérieur, [Ka 84] voir [Sch 86]) dont les complexités théoriques sont polynomiales. L algorithme du simplexe, bien qu'il puisse être, dans le cas le pire, de complexité exponentielle est, en pratique, plus rapide que la méthode de l ellipsoïde. En revanche, la méthode du point intérieur fournit, elle, dans certains cas, des résultats très compétitifs. Il existe aussi une théorie de la dualité pour les programmes linéaires, qui permet, à partir d'un programme linéaire P, d'en considérer un autre, D, (que l'on appelle son dual) : p = max{ c.x A.x b, x 0} (P) d = min{ b.u u.a c, u 0} (D) L'étude conjointe de ces deux programmes peut être particulièrement intéressante, car on a la relation d'ordre suivante : soit x une solution réalisable du programme primal P et u une solution du programme dual D, alors : c.x p = d b.u Ainsi, dans le cas où les deux programmes sont réalisables, leurs valeurs optimales coïncident. La connaissance de deux solutions x et u permet ainsi d'encadrer cet optimal. Enfin, on prouve encore que si p =, alors D est irréalisable, et réciproquement, si d = -, alors P est irréalisable Programmation entière Le cas de la P.L.N.E., où l'on cherche des solutions à coordonnées entières est beaucoup plus complexe. En effet, d'un point de vue géométrique, le domaine d'optimisation n'est plus un corps convexe continu, mais un ensemble discret de points. Alors que dans le cas précédent, il suffisait de parcourir l'ensemble des sommets du polytope, puisqu'ils correspondaient à des solutions, ici, cette approche n'est plus valable, car les sommets sont en général à coordonnées fractionnelles. 2
21 Sous certaines conditions (quand la matrice A est dite totalement unimodulaire, voir [Sch 86]), les sommets du polytope sont à coordonnées entières. On reconnaît ces situations quand toutes les sousmatrices carrées de la matrice de départ (obtenues en ne retenant qu'une partie des lignes et des colonnes) ont pour déterminant 0, 1 ou -1. De manière plus pratique, on verra que ce type de matrices se rencontre dans le cas de problèmes de flot. Dans le cas général, plusieurs directions sont possibles. = La première consiste à revenir dans le cas des programmes linéaires en modifiant l'énoncé des contraintes sous lesquelles on optimise. Puisque le domaine d'optimisation est constitué des points entiers du polytope, on peut chercher à optimiser la fonction objective, non plus sur le polytope P défini par A, mais sur l'enveloppe convexe, IP de l'ensemble de points entiers à l'intérieur de A. On procède ainsi toujours à une optimisation sur un domaine continu (IP), mais comme la valeur optimale de la fonction objective est toujours atteinte en un sommet de IP, les solutions retournées (par exemple, par l'algorithme du simplexe) sont des points à coordonnées entières de P. IP P Figure 1-1: le polytope d'optimisation P et l'enveloppe convexe IP des points entiers de P L'obtention pratique de cette enveloppe convexe pose problème. D'une part, sa description est généralement complexe, et implique un nombre exponentiel d'inégalités. D'autre part, son obtention est souvent le résultat d'une longue procédure [Go 58], [Chv 73]. Plutôt que de passer directement de P à IP, on peut effectuer cette réduction de manière plus incrémentale. Il suffit en effet de disposer d'une procédure capable de générer une à une des facettes de IP. On procède de la sorte : on commence par résoudre le problème P dont la solution optimale est p 1 ; ensuite, soit p 1 est à coordonnées entières auquel cas p 1 est aussi la solution optimale de IP, soit p 1 n'est pas à coordonnées entières et on cherche un hyperplan séparant le domaine IP du point p 1 (par exemple, une facette de IP). On rajoute cette inéquation à la description de P, on résout ce nouveau problème, et on obtient un point p 2, et ainsi de suite... Cette méthode est appelée la génération de plans de coupes. = L'autre possibilité consiste à n'utiliser que la valeur optimale du programme P. Le problème P correspondant à la même maximisation que le problème IP, mais sur un domaine moins contraint, sa valeur maximale est une borne supérieure de celle de P. P est ainsi appelé une relaxation du problème IP (on a relâché les contraintes d'intégrité de la solution), qui permet d'approcher la valeur optimale du programme P. Cette technique de relaxation qui consiste à résoudre un problème plus simple que le problème de départ est une technique générale en algorithmique pour obtenir des bornes supérieures (si on maximise). Par exemple, la technique de la relaxation Lagrangienne consiste à modifier le problème de 3
22 départ de manière paramétrique pour obtenir une série de telles bornes et à chercher ensuite la valeur du paramètre donnant la borne la plus précise possible Implémentation pratique Le succès de la méthode du simplexe en programmation linéaire vient de ce que l'on peut en proposer une implémentation simple et très efficace. On a pour habitude de représenter les vecteurs de la base et la sous matrice carrée inversible de A correspondant à la base courante dans une grande matrice de taille n m, appelée tableau. On montre que l'opération qui consiste à faire sortir un vecteur de la base et à en faire rentrer un nouveau peut se faire de manière simple et incrémentale (avec peu d'opérations), en modifiant le tableau par des manipulations matricielles élémentaires. Cette série d'opérations consiste à faire apparaître un pivot dans le tableau : par une série de combinaisons linéaires de lignes, on fait apparaître des 0 à toutes les positions d'une colonne, sauf à la place du pivot, qui contient un 1. On peut même améliorer encore cette méthode en travaillant sur une structure plus compacte, un tableau réduit de taille m m. La mise à jour de ce tableau nécessite à chaque étape de pivot d'aller consulter la colonne de la matrice initiale A correspondant à la variable pivot. Cette méthode est donc particulièrement intéressante quand n est beaucoup plus grand que m (quand le nombre de variables est beaucoup plus grand que le nombre de contraintes), car au lieu de manipuler tout l'ensemble de contraintes, on ne fait appel qu'à une colonne de la matrice de contraintes à la fois. Cette technique qui s'appelle la génération de colonnes, a un grand intérêt pratique : d'une part, elle permet de limiter la complexité des opérations de pivot, même pour des valeurs importantes de m; d'autre part, elle permet de limiter la place mémoire requise pour l'algorithme du simplexe avec de grosses matrices (on peut éventuellement même éviter de stocker explicitement cette matrice, et se contenter de générer à la volée les colonnes intéressantes). 1.2 Programmation par contraintes Langage de modélisation, Solutions Le cadre de la programmation par contraintes permet lui aussi de modéliser des problèmes de satisfiabilité (trouver une solution) et d'optimisation (trouver la meilleure solution, celle qui maximise ou minimise un certain critère) par un ensemble de variables à affecter tout en respectant des formules contraignant les valeurs de ces variables. La différence vient du langage offert pour l'expression des contraintes. A chaque variable, on associe un domaine de valeurs possibles, fini ou non. On peut ainsi spécifier un intervalle de réels ou d'entiers comme en PL., mais aussi un ensemble d'entiers énumérés, un Booléen, ou un ensemble d'objets quelconques. Une solution est la donnée d'une assignation, pour chacune des variables, d'une valeur dans son domaine. Les contraintes sont des formules algébriques, a priori quelconques, portant sur les variables. En voici deux exemples : x > 2 = (y 3 x + y = z) == = p Person, p.father.age > p.age = = p Doctor, AllDifferent({A.schedule A in Appointments(D)}) Pour les problèmes d'optimisation combinatoire qui nous intéressent, on utilisera les domaines finis et en particulier les ensembles et intervalles d'entiers (on ne considérera donc pas le cas de domaines infinis). Par contre, on ne se limitera pas aux simples contraintes arithmétiques élémentaires, mais on évoquera de nombreux autres types de contraintes, (contraintes symboliques, contraintes globales, utilisation de 4
23 quantificateurs, etc. ). Pour les problèmes d'optimisation, on représentera le critère à optimiser par une variable de domaine, liée par une formule (la fonction objective) aux autres variables du problème. Du point de vue de la modélisation d'un problème combinatoire, l'expression sous forme de contraintes est généralement plus lisible (grâce à l'utilisation d'objets et de champs) et plus concise (grâce à l'utilisation de contraintes symboliques permettant d'exprimer des relations complexes entre des variables) qu'en programmation linéaire. Le formalisme utilisable pour définir des contraintes n'est a priori pas limité. Depuis les premières utilisations de la programmation par contraintes pour la résolution de problèmes combinatoires, de nouvelles constructions ont été régulièrement proposées (contraintes element, opérateur de cardinalité [vhd 91], contraintes globales d'optimisation combinatoire -cumulative, cycle, diffn-, [BC 94], nouvelles versions des contraintes de différence [Ré 94, CL 97b], contraintes géométriques, topologiques [SV 97], etc.) Domaines, propagation Si un programme par contraintes peut utiliser des langages (algèbres) différents pour l'expression des contraintes (arithmétique, opérations sur les listes, opérateurs Booléens, etc. ), ces expressions utilisent toutes un mécanisme commun : la propagation de contraintes. D'un point de vue logique, on considère généralement qu'un programme par contraintes est composé d'un état courant et d'opérateurs de transformation sur cet état. On représente l'état (appelé store) par un ensemble de formules logiques (définition d'objets, de domaines, de relations) et les transformations sont des mécanismes d'inférence, capables de déduire de nouveaux faits logiques qui n'étaient pas explicitement présents dans l'état, aussi bien que de simplifier l'expression de l'état courant (pour une présentation formelle du cadre de la programmation logique par contraintes, on renvoie le lecteur à [JL 87], [JM 94], [Fa 97]). D'un point de vue plus opérationnel, on peut considérer qu'un état est représenté par un ensemble de variables auxquelles sont associés des domaines des valeurs possibles. Toutes les contraintes non élémentaires peuvent alors être vues comme des opérateurs de transformation d'état, capable d'enlever des domaines courants certaines valeurs impossibles. Par exemple, pour le problème élémentaire suivant x [4, 12], y [0, 8], x + y = 10 la contrainte x + y = 10 permet de réduire les domaines des deux variables x et y de la manière suivante : x [4, 10], y [0, 6]. Le comportement de chaque contrainte est ainsi défini comme la suppression de valeurs des domaines sous certaines conditions. C'est ce qui permet la combinaison aisée de contraintes différentes, puisque le comportement de chacune peut être décrit indépendamment des autres. Ces mécanismes de réduction de domaines peuvent effectuer des inférences plus ou moins efficaces [vh 89]. Citons, par exemple la propagation de valeurs (aussi appelée forward-checking). Quand parmi les n variables impliquées par une contrainte, les valeurs de n-1 d'entre elles sont connues, on peut réduire le domaine de la dernière en ne retenant dans le domaine que les valeurs compatibles avec les n-1 autres valeurs. Par exemple, si x,y {4.. 10} et x+y = 10, de l'égalité x = 4 on peut déduire y = 6. le filtrage sur les domaines (dont certaines versions sont appelées look-ahead). Il s'agit d'enlever des domaines des valeurs incompatibles avant même qu'aucune valeur ne soit connue définitivement. Ceci permet à une contrainte c d'éliminer du domaine d'une de ses variables v toutes les valeurs individuelles qui seraient incompatibles avec les domaines de valeurs des autres variables de c. Ainsi par exemple, si l'on a 5
24 x {0.. 10}, y {0.. 10}, x + 2y < 12 et y - x > 3 la première inégalité permet de réduire le domaine de y à [0.. 5], ce qui, grâce à la seconde inégalité permet de réduire celui de x à {0,1}. La propagation d'une contrainte sur les domaines finis correspond ainsi à une analyse de cas, plus ou moins élaborée, permettant d'éliminer à l'avance certaines valeurs des domaines. Ces mécanismes de propagation permettent de répercuter, sur les domaines, l'information contenue dans les contraintes. Réduire les domaines permet d'instancier des variables (quand le domaine est réduit à un singleton) ou de détecter des inconsistances (quand un domaine est vide). La phase complète de propagation, elle, peut soit examiner les contraintes de manière isolée, les unes après les autres (il s'agit alors de la simple cohérence d'arc [Ma 77]), soit considérer plusieurs contraintes à la fois pour raisonner sur la conjonction de contraintes (on peut ainsi considérer la cohérence de chemins). Par exemple, si l'on considère l'ensemble de contraintes suivant : x {0,1}, y {0,1}, z {0,1}, x y, x z, y z l'examen séparé ou 2 par 2 des variables ne permet de faire aucune réduction, alors que l'examen global des 3 variables permet de remarquer qu'il n'existe aucune solution au problème. L'opération de propagation de contraintes consistant ainsi à enlever des valeurs de domaines, on peut l'appliquer plusieurs fois de suite et enlever ainsi encore d'autres valeurs. Formellement, on peut définir la propagation d'un ensemble de contraintes comme un calcul de point fixe sur les domaines avec des règles de réduction associées à chacune des contraintes [Ap 97]. Cette opération est correcte (les valeurs enlevées des domaines n'apparaissent dans aucune solution), mais généralement incomplète. Ainsi, certaines déductions qui sont correctes ne sont pas faites, et de manière similaire, certaines situations ne menant aucune solution ne sont pas jugées inconsistantes Interprétation abstraite. La gestion des domaines ainsi que ces mécanismes de filtrage deviennent parfois un peu lourds. Dans ce cas, on préfère raisonner sur une approximation des domaines. Pour les entiers, on peut ainsi se contenter de ne représenter les ensembles de valeurs possibles que par des intervalles (on perd l'information de domaines à trous ). On peut encore ne retenir que le(s) signe(s) des valeurs possibles. On se place donc dans un cadre abstrait puisqu'on approxime des ensembles de valeurs (ici, des ensembles d'entiers) par une valeur abstraite (par exemple, un intervalle ou une partie de l'ensemble à trois éléments {négatif, nul, positif}). On fait ainsi une interprétation abstraite du programme [CC 77], puisqu'on interprète le comportement du programme dans un domaine autre que le domaine des valeurs effectivement prises. L'intérêt de cette transformation est d'offrir une représentation simplifiée mais très synthétique de l'information disponible, sur laquelle on peut mettre en œuvre des mécanismes de raisonnement. On peut ainsi transposer les outils de propagation de contraintes et définir une version abstraite des opérateurs de contraintes sur les entiers. On peut ainsi définir une arithmétique d'intervalles, en définissant, par exemple l addition,, par [a,b] [c,d] = [a+c,b+d]. On peut aussi exprimer la multiplication, dans l'algèbre à trois éléments, par positif positif = positif, positif négatif = négatif, etc. On peut alors utiliser, dans ces domaines abstraits, les mêmes outils et algorithmes de propagation que dans les domaines concrets. Ce raisonnement est licite du moment que les mécanismes d'abstraction forment une correspondance de Galois entre les domaines concrets et abstraits [Co 94]. On dispose alors d'un opérateur d'abstraction et d'un opérateur de concrétisation pour passer des valeurs concrètes aux valeurs abstraites (et réciproquement) ainsi que d'opérateurs de propagation dans le domaine abstrait. On impose alors que le résultat du filtrage des domaines abstraits soit une abstraction du résultat du filtrage 6
25 des domaines concrets. Par exemple, pour le cas d'une approximation des domaines par des intervalles, si x A, y B, et si I (resp. J) est l'abstraction sous forme d'intervalle de A (resp. B) alors on impose que x + y I J. Ce mécanisme permet d'accélérer les calculs de points fixes de propagation. A titre d'exemple, on va comparer trois types de propagation sur le programme suivant : x [-2, 4], y [3, 7], z [-30, -4], x y = z la propagation par cohérence d'arcs sur les domaines énumérés donnera comme résultat : x {-2, -1}, y ={4, 5, 6, 7}, z {-14, -12, -10, -8, -7, -6, -5, -4} la propagation par cohérence d'arcs sur les domaines abstraits par des intervalles d'entiers donnera comme résultat : x [-2, -1], y [4, 7], z [-14, -4] la propagation par cohérence d'arcs sur les domaines abstraits dans l'algèbre {négatif, nul, positif} donnera comme résultat x < 0, y > 0, z < 0, et donc, en revenant dans l'algèbre de départ (concrétisation) fournira le résultat suivant : x [-2, -1], y [3, 7], z [-30, -4] Contraintes inférées, simplifications. Ainsi qu'on l'a indiqué plus haut, on peut considérer que l'état d'un programme de contraintes est un ensemble de formules logiques et que la propagation de contraintes ne consiste qu'à affiner cet ensemble de formules en l'enrichissant. La plupart des conséquences des étapes de propagation peuvent être directement répercutées sur les domaines. On peut cependant parfois vouloir garder sous forme logique certaines déductions. On peut citer deux familles de conclusions qui peuvent résulter d'une phase de propagation : = Arrivé à un certain stade de réduction des domaines, certaines contraintes deviennent vraies quelles que soient les valeurs sélectionnées dans les domaines. Dans ce cas, leur propagation n'apportera plus jamais aucune conclusion. On peut ainsi les faire disparaître. Il s'agit d'une forme de simplification. Par exemple, l'ensemble de contraintes x + y > 3, x + y > 5 peut se réécrire en x + y > 5. La seule utilité de ce type de conclusion est de permettre de simplifier l'expression d'un état et de rendre plus rapide les étapes de propagation suivantes. = Certaines contraintes peuvent être déduites d'autres, par des manipulations formelles. Par exemple, on peut déduire de l'ensemble de contraintes x + y + z = t, x + y > 3, y + z > 4 et z + x > 5 que t > 4 (en sommant les trois inéquations, puis en divisant par deux chacun des membres). Des mécanismes similaires peuvent être appliqués aux disjonctions. Par exemple, on peut déduire de x > 3, y < 2, (x < y - 1 x > y + 2) que x > y + 2 (car x > 3, y < 2, x < y - 1 n'admet aucune solution). 1.3 Recherche arborescente globale Si l'on compare les deux formalismes décrits jusqu'à présent : programmation linéaire et programmation par contraintes, leurs algorithmes dédiés sont incomplets pour les problèmes à valeurs entières. En effet, le simplexe fournit généralement une solution à valeur fractionnelle, et la propagation de contraintes conduit à un état où certains domaines comportent encore plusieurs valeurs. Pour résoudre de manière exacte ces problèmes, on est amené à développer des arbres de recherche. 7
26 1.3.1 Arbre de recherche Que ce soit dans le cadre de la P.L.N.E. ou de la P.P.C., après avoir appliqué l'algorithmes (simplexe ou propagation) trois cas peuvent se présenter : = soit le problème est insatisfiable. En P.L.N.E., le polytope est vide, le simplexe ne trouve aucune solution basique réalisable; en P.P.C., le domaine d'une des variables est vide. = soit on est arrivé à une solution. En P.L.N.E., ceci se produit lorsque la solution réalisant l'optimum est à coordonnées entières; en P.P.C., ceci se produit lorsque toutes les variables sont instanciées. = dans les cas restants, l'état courant n'est pas une solution acceptable. C'est le cas en P.L.N.E. quand une variable de la solution optimale a encore une valeur fractionnaire; c'est le cas en P.P.C. quand il reste une variable non affectée. On sélectionne alors une telle variable non conforme et on pose un point de choix en essayant d'affecter arbitrairement une valeur entière à cette variable. Ainsi, on crée autant de sous-problèmes qu'il y a de valeurs dans le domaine de la variable. Pour chacun de ces sous-problèmes indépendants, on recommence récursivement l'algorithme (propagation ou simplexe). Les sousproblèmes conduisant à des impossibilités sont abandonnés, et on garde soit la première solution que l'on trouve (si on ne cherche qu'une solution), soit la meilleure parmi toutes (pour un problème d'optimisation). L exécution d'un programme suit ainsi le parcours d'un arbre où les noeuds sont étiquetés par des variables et où les branches correspondent aux assignations de chacune des valeurs possibles pour ces variables. Les feuilles de l arbre correspondent soit à des solutions soit à des échecs. La taille des arbres de recherche est bornée par D V, où V est le nombre de variables, et D la taille maximale des domaines. Cependant, les arbres sont en pratique plus petits, car, en P.P.C., les étapes de propagation écartent des valeurs, et en P.L.N.E., le simplexe fournit des solutions avec des valeurs entières, avant que toutes aient été affectées. Les points de choix décrits jusqu'à présent correspondent à l'assignation d'une valeur à une variable. En fait, un point de choix peut être n'importe quelle procédure qui sépare un problème en n sous-problèmes, de sorte que la résolution du problème de départ soit équivalente à la résolution des n sous-problèmes. On peut par exemple créer un point de choix binaire en posant une contrainte (une inégalité linéaire, dans le cadre P.L.N.E.) et sa négation dans deux branches distinctes. On peut aussi effectuer des affectations de valeurs dans un domaine abstrait (par exemple, décider si une variable prend une valeur positive ou négative, sans décider de la valeur en question). Trois points sont déterminants pour la construction et le parcours de l arbre: sur quelle décision poser le point de choix?, dans quel ordre parcourir les différentes branches?, quels calculs effectuer dans chacune des branches? 1. Le choix du point de branchement peut être effectué selon de nombreux critères. Les décisions du début de l arbre (à faible profondeur) sont privilégiées par rapport aux décisions prises à profondeur plus importante qui sont examinées de multiples fois (dans un arbre binaire complet le nombre de noeuds à profondeur n est 2 n ), ce qui peut entraîner des calculs redondants. Le choix des décisions prioritaires est donc crucial; il peut se porter sur : = les objets les plus lourds. Cette méthode est intéressante quand l'utilisateur a une vision hiérarchique du problème ou pense que certaines décisions sont capitales par rapport aux autres. Il s'agit donc d'un critère entièrement dépendant du problème. Certaines techniques, considérant les variables qui apparaissent dans le plus grand nombre de contraintes possibles, s'inspirent de cette méthode. 8
27 = les variables de plus petit domaine. Ce principe est connu sous le nom de first-fail [Fa 97]. L idée est de faire en sorte que les échecs éliminent des parties aussi grandes que possible de l arbre. On cherche ainsi à minimiser le nombre de noeuds aux profondeurs où les échecs sont détectés : la stratégie consiste à avoir des arbres les plus fins possible. En général, ce principe est assez efficace. = En P.L.N.E., on voudra chercher à fixer la valeur de variables qui sont le plus loin possible de valeurs entières, pour que le changement d'une solution à la solution suivante ait le plus grand impact possible sur la fonction objective. = En P.P.C., on pourra chercher à faire les choix dont la propagation a un impact maximal. Ceci correspond à une vision entropique du système, où l'on cherche à diminuer le nombre de degrés de liberté du système le plus rapidement possible. = et d'autres méthodes ad hoc L'ordre de parcours des branches importe dans le cas de problèmes de satisfiabilité, mais moins dans le cas de problèmes d'optimisation (puisqu'on visite tout l'arbre). On cherche alors à visiter en priorité les branches qui semblent mener aux bonnes solutions (principe best first [Fa 97]). 3. Enfin, le point crucial est de savoir quelle quantité de travail faire à chaque choix. La solution la plus simple (generate and test) consiste à ne rien propager du tout, et, une fois que l'on se trouve aux feuilles de l'arbre (quand toutes les affectations ont été faites), à tester si l ensemble des contraintes est vérifié ou non. C'est cependant la solution la moins intéressante, car elle oblige à parcourir tout l'arbre (les D V nœuds). Plus on sait propager, plus tôt on pourra détecter des inconsistances et moins l'arbre parcouru aura de nœuds. Dans la pratique, tout l art des algorithmes de recherche consiste à savoir propager suffisamment d information (pour avoir des arbres avec peu de noeuds) suffisamment vite (pour passer peu de temps à chaque noeud) et obtenir par ce compromis des algorithmes rapides. La même question se pose en P.L.N.E., où l'on peut affiner la description du problème à résoudre par la génération de plans de coupe (branch & cut) à chaque noeud de l'arbre Optimisation par séparation et évaluation Pour les problèmes de satisfiabilité, le parcours de l arbre s arrête à la première solution trouvée. Pour les problèmes d optimisation (disons que l on cherche à maximiser une fonction f), deux stratégies sont possibles. On peut, à chaque solution trouvée S, de coût c, recommencer une nouvelle recherche arborescente, en rajoutant à la définition du problème, la contrainte f > c. L'autre possibilité consiste à faire une unique recherche arborescente, mais en rajoutant dynamiquement, à chaque fois que l'on trouve une nouvelle solution S, la contrainte f > f(s) à l ensemble des contraintes (il faut rajouter cette contrainte d'optimisation à tous les noeuds de l'arbre en cours d'examen). Ceci permet ainsi de ne chercher que des solutions qui améliorent la fonction objective; quand tout l arbre a été parcouru, on a trouvé une suite de solutions croissante pour f dont on a prouvé que la dernière est optimale. Cette technique est connue sous le nom de procédure par séparation et évaluation (branch and bound) [Sch 86],[GM 95]. La séparation ( branching ) consiste à poser un point de choix pour séparer le problème en plusieurs sous-problèmes distincts, l évaluation de la fonction objective ( bounding ) permet de ne pas considérer (élaguer) les parties de l arbre conduisant à une solution non-optimale. Les procédures par séparation et évaluation peuvent aussi évaluer à chaque nœud de l'arbre, des bornes inférieures et supérieures de la valeur de l'optimum dans le sous-arbre courant. Le fait d'associer à chacun des noeuds de l'arbre de recherche un intervalle pour encadrer l'optimum permet notamment de guider la manière dont l'arbre va être exploré. 9
28 Ces algorithmes par séparation et évaluation forment la famille principale des algorithmes exacts d'optimisation. L'utilisation systématique des bornes souligne l'intérêt des méthodes de relaxation de problèmes Relaxation Lagrangienne Ainsi, lors d'une procédure d'optimisation par séparation et évaluation, on se sert d'une borne pour laisser de côté les parties de l'arbre où cette borne indique que les solutions ne seront pas meilleures que celles déjà trouvées. Dans le cadre de la P.L.N.E., ces bornes sont obtenues par le simplexe, c'est à dire en ignorant les contraintes d'intégrité sur les variables. Ce mécanisme qui consiste à relaxer une partie des contraintes est très général. Le but de ce mécanisme est de se ramener à un problème que l'on sait bien résoudre (comme les programmes linéaires dans les rationnels, pour lesquels on dispose d'un algorithme polynomial). On peut cependant être amené à relaxer d'autres contraintes. Nous allons décrire le mécanisme général de la relaxation Lagrangienne (on renvoie le lecteur à [Bea 93] pour une présentation plus détaillée). Supposons que l'on ait le programme linéaire suivant (P) à résoudre : min c.x A.x = b, C.x d, x 0 (P) Supposons encore que le problème P' obtenu en oubliant la première famille de contraintes soit un problème facile (par exemple, un problème que l'on sait résoudre par un algorithme polynomial), min c.x C.x d, x 0 Alors, on introduit le problème paramètré P λ en remontant la contrainte qui pose problème (ici A.x = b) dans la fonction objective : min( c.x λ.(b A.x) ) C.x d, x 0 (P λ ) On peut alors facilement résoudre des problèmes P λ pour différents vecteurs λ de paramètres positifs et en tirer des informations sur P. Notons ξ une solution optimale de P et F = c.ξ la valeur optimale de P, et similairement, notons ξ λ une solution optimale de P λ et F λ = c.ξ λ - λ(b - A.ξ λ ) la valeur optimale de P λ. Alors, ξ étant une solution quelconque pour P λ, nécessairement, F λ = c.ξ λ λ( b A.ξ λ ) c.ξ λ( b A.ξ )= c.ξ = F On peut donc borner F grâce aux F λ : max λ ( F λ ) F On cherche ainsi à calculer le maximum de F λ pour obtenir une borne inférieure de F. Or λ est un vecteur réel (appelé vecteur des multiplicateurs de Lagrange) et trouver le maximum exact de F λ est un problème d optimisation continue. Malheureusement, la fonction F λ n'est pas différentiable, et l'on ne peut donc pas trouver son maximum par un calcul de gradient. La méthode que l'on applique consiste à calculer un sousgradient c'est à dire une direction (dans l espace des multiplicateurs) dans laquelle la valeur objective de F λ augmente. On remarque que le vecteur (b - A.ξ λ ) est toujours un sous-gradient des multiplicateurs (λ). En faisant évoluer les multiplicateurs de Lagrange (le vecteur λ)=dans ce sens, on converge vers le (P ) 10
29 maximum de F λ. Suivant les cas, on cherchera la valeur exacte de ce maximum, ou on se contentera d'une approximation (qui reste une borne inférieure de F). Au cours de la résolution des problèmes P λ, il arrive que la solution trouvée ξ λ soit une solution admissible de P (si jamais A.ξ λ = b ). Alors, F = c.ξ c.ξ λ = F λ + λ( b A.ξ λ )= F λ la solution ξ λ est dans ce cas une solution optimale de P, puisque F λ est borne inférieure et supérieure de F Parcours de l'arbre - Implémentation - Backtrack. L'algorithme de recherche arborescente une fois décrit, il reste encore de nombreuses possibilités d'implémentation. Par exemple la question se pose du parcours de l'arbre de recherche. A priori, de nombreuses possibilités de parcours sont offertes (en largeur d'abord, en profondeur d'abord, certaines stratégies dynamiques comme A*, utilisant les valeurs des bornes, etc.). [Pea 84], [HG 95], [HeKoe 96]. De plus, le parcours d'un arbre de recherche est une opération plus lourde que la simple visite d'une structure de données arborescente, puisqu'à chaque nœud est associé un état (le contenu des domaines des variables en P.P.C., le sommet courant ou les variables de la base en P.L.N.E.). D'un nœud à un autre, il faut donc sauvegarder l'état puis restaurer le nouvel état. Dans les faits, la plupart du temps, les arbres de recherche sont parcourus le plus souvent en profondeur d'abord. Ceci s'explique pour une raison simple d'implémentation : le nombre d'états à stocker est borné par la profondeur de l'arbre (en pratique, logarithmique par rapport au nombre total de noeuds). Pour des problèmes où l'état courant contient potentiellement des données de taille importante, on ne peut gérer simultanément en mémoire de trop nombreux états. Toutefois, des parcours autres que le parcours en profondeur d'abord peuvent s'avérer intéressants, en particulier ceux où l'on ne garde pas explicitement les noeuds, mais où l'on recalcule les états (comme le parcours à profondeurs successives [Ko 85], ou certaines versions de A*). Pour le parcours en profondeur d'abord, on va considérer une pile d'états (qui correspondent à des mondes hypothétiques ou encore des versions du sytème). À chaque noeud, on crée un nouveau monde (on prend une photographie instantanée de la base de donnée). Après avoir exploré le sous-arbre d'un noeud, on revient au monde précédent (l'état du noeud père). Ce mécanisme de retour en arrière s'appelle le backtrack. Du point de vue de l'implémentation, on va éviter de stocker des états complets à chaque noeud. Dès que le nombre de variables est important, cette méthode devient trop coûteuse en place. On préfère stocker les mondes de manière incrémentale (on ne garde dans un noeud que l'ensemble des différences entre son état propre et celui de son père). La solution généralement retenue consiste à stocker en dur dans les objets les valeurs du monde courant et à garder en mémoire sur une pile, les modifications faites d'un monde à l'autre. Cette solution engendre un coût à l'écriture de valeurs et lors du backtrack, mais ne pénalise pas la lecture de valeurs.. Ainsi, dans un parcours en profondeur d'abord, pour descendre le long des arêtes de l'arbre, on initialise une zone de mémoire vierge sur la pile dans laquelle on va stocker les modifications faites dans le noeud-fils (par exemple, en P.P.C., on réduira des domaines). Pour remonter le long d'une arête et revenir au monde précédent ("backtracking"), on restaurera les anciennes valeurs grâce à cette mémoire, que l'on pourra ensuite dépiler. 11
30 1.4 Algorithmes approchés, recherche locale Nous venons de décrire deux cadres algorithmiques (PL. et P.P.C.) pour résoudre de manière exacte des problèmes d'optimisation. Nous allons maintenant présenter rapidement d'autres familles d'algorithmes pour résoudre de manière approchée ces problèmes d'optimisation, c'est à dire permettant de trouver de bonnes solutions (non nécessairement optimales) Heuristiques - algorithmes gloutons Les algorithmes les plus simples pour résoudre un problème d'optimisation décomposent généralement le problème global en une série de petits problèmes locaux, de sorte que la fonction objective (à optimiser) soit une agrégation simple de coûts élémentaires sur les problèmes locaux. Par exemple, le problème du voyageur de commerce qui consiste à trouver un trajet de longueur minimale passant par n villes (et qui sera étudié au chapitre 4) peut être décomposé en n petits problèmes, un par ville, chacun responsable de la sélection d'une liaison entre deux villes. Comme la longueur totale du trajet est la somme des longueurs des n liaisons, il semble pertinent de choisir les liaisons élémentaires de longueur aussi courtes que possible pour obtenir un trajet. S'appuyant sur cette décomposition, un algorithme glouton construit la solution de proche en proche, en résolvant un par un les problèmes locaux, en optimisant le critère local à chaque étape. Ainsi, pour le cas du voyageur de commerce, un algorithme glouton commence par choisir l'arête de poids minimal, puis sélectionne de proche en proche n arêtes, en choisissant toujours celle de poids minimal parmi toutes celles qui ne créent pas un cycle de longueur k < n avec les arêtes déjà sélectionnées. Un autre algorithme glouton, basé sur la même décomposition, fixerait un sommet de départ, choisirait de le relier à son plus proche voisin, et recommencerait à partir de ce voisin, etc. De manière générale, les algorithmes gloutons s'appliquent à des problèmes d'optimisation tels que le problème de satisfiabilité sous-jacent (qui consiste simplement à trouver une solution, bonne ou mauvaise) soit facile. Sinon, la procédure peut s'arrêter en échec, arrivant à un sous-problème devenu insoluble. Ce cas peut par exemple se présenter si l'on considère le problème du voyageur de commerce dans un graphe incomplet. Enfin, on notera qu'un algorithme glouton peut toujours être décrit comme un parcours sans backtrack dans un arbre de recherche. En effet, les sous-problèmes peuvent être considérés comme des points de choix, où l'on prend une décision parmi plusieurs possibles. Le parcours de cet arbre est très limité, puisqu'on n'effectue aucun retour-arrière : on ne fait donc que descendre le long d'une branche Recherche locale L'autre grande famille de techniques de résolution est constituée des algorithmes de recherche locale (par opposition aux algorithmes décrits précédemment, qui effectuent une recherche globale en énumérant tout l'ensemble des solutions). Le principe des algorithmes de recherche locale consiste à se déplacer dans l'espace des solutions, en allant de solution en solution voisine. On est ainsi amené à considérer des voisinages de solutions (un ensemble de solutions proches ). Par exemple, l'algorithme du simplexe, présenté plus haut, est un algorithme d'optimisation locale, puisqu'on se déplace de sommet en sommet voisin sur le polytope d'optimisation. Ces techniques d'optimisation n'explorant pas tout l'espace des solutions, fournissent généralement un résultat correspondant à un optimum local et non global du système, à quelques exceptions notoires près, comme le simplexe, ou les algorithmes de flot. Un algorithme d optimisation locale comporte deux composants: la définition d un voisinage (permettant d'obtenir les solutions voisines d'une solution donnée) et la donnée d un contrôle sur l exploration de ce voisinage. La notion la plus élémentaire de contrôle est l équivalent des algorithmes gloutons pour les 12
31 procédures de construction directe, et consiste à aller d une solution à une solution voisine qui soit meilleure que la solution courante (éventuellement, la meilleure de celles-ci), jusqu à ce que l'on l'algorithme s'arrête sur un optimum local (aucune des solutions voisines ne présente une amélioration). Cette stratégie d'améliorations successives, appelée méthode de descente (ou hill-climbing), est celle qu'utilise le simplexe. En dehors de ce cas simplifié déterministe, les procédures d optimisation locale consistent le plus souvent en une marche aléatoire dans l'espace des solutions. Parmi les procédures de contrôle, citons trois procédures (méta-heuristiques) classiques : la recherche tabou, les algorithmes génétiques et le recuit simulé. le recuit simulé [KGV 83] est une marche aléatoire dans l'espace des solutions où l'on autorise les transitions qui améliorent la valeur de la solution, mais aussi certaines de celles qui la détériorent, dans l'espoir de franchir les pics de potentiel qui séparent les optima locaux. Le paramètre de contrôle (responsable de l autorisation des mauvaises transitions) est appelé température et décrit en quelque sorte l'état d'agitation du système : plus il est élevé, plus de telles transitions sont acceptées. Il. On commence la recherche avec un paramètre de température élevé, que l'on baisse au fur et à mesure. L idée de cette méthode est d inspiration physique (processus de cristallisation). la recherche tabou [Glo 89] est une marche aléatoire avec mémoire. L idée est de garder en mémoire les solutions visitées et de s interdire de revenir sur ses pas. Ce mécanisme permet de sortir d optima locaux (en remplissant la mémoire de tous les états attractifs d'un optimum local, on est ensuite forcé d'aller explorer d'autres états). les algorithmes génétiques [Ho 75] raisonnent sur un ensemble de solutions et un voisinage est défini par des mélanges de solutions entre elles ( croisements ). Le but de la combinaison de solutions est d'arriver à garder les bons aspects de deux solutions pour produire une solution encore meilleure. L'inspiration de cette méthode vient de l'étude de l'évolution des populations. On pourrait citer encore d'autres cadres d'optimisation d'inspiration naturelle (les algorithmes de fourmis, par exemple). Cependant, une caractéristique commune à ces propositions est la difficulté de leur mise en oeuvre, qui nécessite des réglages subtils (température, longueur de la liste tabou, taux de mutation spontané des gènes, etc...). Quelques conclusions semblent émerger de l'utilisation de ces méthodes : Ces méthodes fonctionnent bien quand les voisinages considérés sont pertinents pour le problème. Dans ce cas, l'addition d'un facteur aléatoire dans la marche d'optimisation locale semble fréquemment améliorer les performances d'une exploration déterministe. De plus, parmi toutes les propositions citées, la recherche tabou, semble être celle qui fournit les meilleurs résultats pour les problèmes d'optimisation discrète qui nous intéressent Relaxation et réparations Lorsque l'on cherche à résoudre des problèmes pour lesquels trouver une solution est déjà une tâche difficile, une possibilité alternative aux méthodes complètes consiste à relaxer (mettre de côté) certaines contraintes du problème : on considère alors un problème moins contraint, dont les solutions sont plus faciles à trouver. On peut ainsi exprimer le problème de satisfiabilité initial en un problème d'optimisation dans le cadre relaxé, en minimisant, par exemple, le nombre de contraintes violées. L'application de techniques génériques d'optimisation globale sur ces problèmes relaxés ne fournit pas de manière systématique de bons résultats. Un direction plus prometteuse fait appel à des techniques d'optimisation locale pour diminuer les violations de contraintes de la solution approchée, comme par exemple, l'algorithme G-SAT [SLM 92] pour le problème 3SAT (satisfaction de clauses à trois littéraux). 13
32 Dans ce cadre, on parle d'algorithmes de réparation, puisqu'ils suppriment des défauts de la solution au problème relaxé. On notera qu'il peut être intéressant d'incorporer dans un algorithme de réparation, en plus de techniques génériques d'optimisation locale et de marche aléatoire basées sur des mouvements simples, des transformations déterministes, basées sur une analyse fine de la solution, du point de vue du domaine applicatif. Dans le cadre de problèmes industriels réels, les experts de l'application disposent généralement de critères et procédures, plus ou moins formalisés, pour améliorer une solution. 1.5 Un exemple : la couverture d ensembles Le problème de la couverture d'ensemble est le suivant: on se donne des éléments E = {e 1,..., e n } et des ensembles d éléments S = {S 1,..., S k } (les S j sont des parties de E). On cherche une sous-famille T de S qui couvre tout E, c est-à-dire que pour tout e i E, il existe un ensemble S j T tel que e i S j. On cherche une telle famille T de cardinalité minimale. Ces problèmes de couverture d'ensembles sont très étudiés dans la littérature de recherche opérationnelle et des algorithmes basés sur un modèle linéaire sont capables de résoudre des problèmes de grande taille (500 éléments, 5000 ensembles) [Bea 93]. Cependant, quelques problèmes particuliers, avec de nombreuses symétries, sont plus difficiles à résoudre (par exemple, le problème de l'ensemble de sommets dominants minimal sur les graphes de tournois de football, pour n = 6, comporte 81 éléments et 729 ensembles et est toujours ouvert, c'est-à-dire irrésolu [DR 96]). Le modèle linéaire est le suivant. Pour chaque ensemble S j, on introduit une variable Booléenne x j {0,1}, dénotant la sélection ou non de S j dans T. On définit la matrice A à entrées a ij {0,1} de sorte que a ij = 1 si et seulement si e i S j. Le problème de couverture d'ensemble peut ainsi être décrit par le programme linéaire suivant : i, min x j j a ij x j j j, x j { 0,1} Pour résoudre ce programme linéaire, on pourra utiliser l'algorithme du simplexe à l'intérieur d'un algorithme de recherche globale procédant par séparation et évaluation : une borne supérieure sera fournie par les solutions réalisables trouvées en cours de recherche, une borne inférieure sera fournie par les valeurs des relaxations linéaires (solutions irréalisables). Cette borne supérieure peut encore être affinée par relaxation Lagrangienne [Bea 87]. Pour résoudre ce problème en programmation par contraintes, nous proposons de raisonner aussi sur ces variables Booléennes attachées à chaque ensemble, indiquant si l'ensemble est retenu dans la famille T ou non. L'algorithme va donc être un algorithme de recherche globale, au cours duquel les ensembles S j S se répartiront en trois catégories : ceux qui sont dans T (que l'on appellera les ensembles sélectionnés), ceux qui n'y sont pas (les ensembles rejetés), et ceux dont le sort n'a pas encore été décidé (ensembles disponibles). De même, on dira que les éléments sont couverts ou non s'ils appartiennent à un ensemble qui est dans T. On maintiendra, pour chaque ensemble disponible, le nombre d'éléments non couverts qu'il contient (on appellera ce nombre l' utilité de l'ensemble), et pour chaque élément non couvert, le nombre d'ensembles disponibles auquel il appartient. Ces informations nous permettront de faire la propagation suivante : = quand un élément non couvert n'appartient plus à aucun ensemble disponible, le problème est irréalisable, 1, 14
33 = quand tous les éléments d'un ensemble disponible sont couverts par ailleurs, on peut rejeter cet ensemble, = quand un élément non couvert n'appartient plus qu'à un seul ensemble disponible, cet ensemble doit être sélectionné. On peut proposer deux modes de construction d'un arbre de recherche pour la résolution du problème : = soit en considérant un ensemble disponible et en lui associant l'alternative binaire de le sélectionner ou de le rejeter = soit en considérant un élément non couvert et en choisissant de le couvrir par un des k ensembles disponibles auxquels il appartient. Dans ce cas, le point de choix produit k branches. Enfin, pour estimer (par défaut) le cardinal de T, on peut ajouter au nombre courant d'ensembles sélectionnés un borne inférieure du nombre d'ensembles disponibles à sélectionner pour pouvoir couvrir les N éléments non couverts. On peut proposer deux formulations d'une telle borne : = on peut diviser N par l'utilité maximale des ensembles disponibles, = on peut classer les ensembles disponibles par utilité décroissante et compter le premier indice à partir duquel la somme cumulée des utilités atteint N. Mentionnons enfin, que l'algorithme de contraintes peut être utilisé sans retours en arrière lors de l'exploration de l'arbre, ce qui donne un algorithme glouton. 15
34 16
35 17
36 Partie A: Contraintes et algorithmes, tour d'horizon de problèmes d'optimisation combinatoire 1
37 2
38 2. Chapitre 2. Couplages et allocation Dans ce chapitre, on va s'intéresser à la résolution de problèmes de couplages bipartis. On présentera une partie du travail publié dans [CL 97b]. Les problèmes de couplages sont importants car ils sont à la base de nombreux problèmes d'allocation de ressources. On présentera trois familles d'algorithmes : des algorithmes provenant de théorie des graphes, basés sur une formulation en termes de flots, un algorithme (la méthode Hongroise) basé sur une modélisation linéaire duale ainsi que des algorithmes de programmation par contraintes. On proposera un enrichissement du cadre habituel de propagation de contraintes, on comparera ces méthodes sur plusieurs variations du problème initial, et l'on envisagera l'utilisation de la méthode Hongroise à l'intérieur d'un système de propagation par contraintes. 2.1 Les problèmes de couplages bipartis Définition Les problèmes de couplages sont, comme beaucoup des problèmes qui seront abordés au cours de ce tour d'horizon des problèmes combinatoires, des problèmes de théorie des graphes. On dit d'un graphe G qu'il est biparti si l'on peut partitionner les sommets en deux ensembles V 1 et V 2 de telle sorte que toutes les arêtes dans le graphe joignent un sommet de V 1 à un sommet de V 2. Un couplage dans un graphe biparti (matching en anglais) est un ensemble d'arêtes 2 à 2 non incidentes dans G. On s'intéresse ici au cas où les deux ensembles de sommets ont même cardinalité n et on cherche un couplage de cardinalité maximale. Par exemple, si le nombre d'arêtes, m, vaut n 2 (le graphe biparti est complet), le couplage maximal est de cardinalité n (on parle dans ce cas de couplage parfait). On peut aussi associer à chacune des arêtes un poids, et à considérer le poids d'un couplage comme la somme des poids de ses arêtes. Le problème de la recherche d'un couplage maximal peut alors être affiné en la recherche du couplage maximal de poids minimal (ou maximal). Ce problème s'appelle aussi le problème des mariages, à cause de la métaphore suivante qui considère V 1 comme un ensemble d'hommes et V 2 comme un ensemble de femmes et les arêtes entre ces ensembles de sommets comme les possibilités de formation de couples. Le problème du couplage maximal peut être celui d'un marieur cherche à apparier le plus grand nombre possible de couples Application des couplages parfaits à l'allocation contrainte. De nombreux problèmes réels d'allocation de ressources peuvent être décrits comme des problèmes de couplages dans des graphes bipartis. Par exemple, si l'on décrit par V 1 un ensemble des machines, et par V 2 un ensemble des tâches à accomplir, et si le graphe représente une relation de compatibilité (liant une machine aux tâches qu'elle peut effectuer), un couplage dans ce graphe représente une affectation de tâches aux machines. Ces problèmes se situent de manière naturelle dans le cadre valué, où l'on associe par exemple, comme poids à l'arête mt, la production (ou la consommation d'énergie) de la machine m quand elle exécute la tâche t. On veut alors distribuer l'ensemble de tâches sur l'ensemble des machines de façon à obtenir une production maximale (ou une consommation minimale). De plus, les problèmes d allocation tâche-machine sont souvent accompagnés de contraintes supplémentaires. Un des facteurs importants de l algorithme retenu doit être sa robustesse par rapport à ces contraintes supplémentaires. A ce titre, nous allons considérer deux variations du problème : La première variation prend en compte la possibilité de remplacement de certaines machines par d'autres. On considère ainsi, que l'atelier, pour se moderniser, peut remplacer k parmi les n machines de départ, par de nouvelles, plus performantes. Ainsi, pour chaque arête mt, on a deux valuations. La première, coût mt reflète la productivité de m sur t et la seconde, coût' mt reflète la productivité de la 3
39 remplaçante de m sur t. Il s'agit donc de trouver une allocation tâche-machine et un sous ensemble M k de k machines tel que le coût de cette allocation (en sommant la fonction coût sur les arêtes non incidentes à M k et la fonction coût' sur les arêtes incidentes à M k ) soit maximal. La seconde variation considère le problème d'allocation du point de vue simultané de deux critères. On peut ainsi associer deux valeurs à une solution, la consommation d'énergie et la production. On peut alors chercher à optimiser un de ces deux critères (disons, maximiser la production) en tout en gardant le second (la consommation d'énergie) en deçà d'un certain niveau. 2.2 Couplages bipartis et flots Pour résoudre les problèmes de couplages bipartis, une solution simple offerte par la recherche opérationnelle consiste à modéliser le problème des couplages comme un problème de flot. Les problèmes de flots ont l'intérêt de pouvoir être résolus de manière exacte par des algorithmes d'optimisation locale, simples et de complexité polynomiale Définition, chaînes augmentantes Les problèmes de flots appartiennent aussi à la théorie des graphes. En fait, il s'agit plutôt de plomberie: On cherche à faire passer un débit maximal à travers un réseau de tuyaux de sections variées. Formellement, on se donne un graphe où l'on a particularisé deux sommets (une source s et un puits p), valué par une fonction de capacité cap. Un flot est une valuation flot sur les arcs (arêtes orientées) vérifiant les deux contraintes suivantes: = Pour tout arc e, flot(e) cap(e) = En tout sommet (sauf la source et le puits), la somme des flots qui arrivent est égale à la somme des flots qui repartent. On définit le flot total comme la somme des flots partant de la source (ou, de manière équivalente, la somme des flots arrivant au puits). On cherche un flot maximal. On peut aussi considérer une version valuée du problème en partant d'un graphe qui a une deuxième valuation coût sur les arêtes. On peut alors définir le coût total du flot comme C = e E flot(e) cout(e) On s'intéresse alors aux flots maximaux de coût minimal. Les algorithmes classiques pour construire un flot maximal partent d'un flot quelconque, s'arrêtent s'il est maximal et l'augmentent sinon. Pour l'augmenter, on cherche une chaîne augmentante, c'est à dire un chemin non saturé, un chemin sur tous les arcs duquel on puisse augmenter le flot Couplages bipartis et flots Le problème du couplage parfait dans un graphe biparti peut se réduire à un problème de flot maximal par la construction suivante: on rajoute deux sommets, une source s et un puits p au graphe biparti. On relie tous les sommets de V 1 à s et tous les sommets de V 2. à p. Tous les arcs du graphe sont de capacité 1. À tout flot entier entre s et t, on peut associer un couplage en prenant l'ensemble des arêtes joignant V 1 et V 2. Les contraintes de capacité sur les arêtes incidentes à s et p garantissent qu'il s'agit bien d'un couplage. Réciproquement, à tout couplage, on peut associer un flot entier. De plus, les flots maximaux correspondent aux couplages maximaux. Ceci vient d'un théorème de théorie des flots [GM95, page 163] 4
40 qui affirme que pour toute fonction de capacité entière, le flot maximal est entier. Ce résultat peut être justifié par le fait que les matrices de graphes orientés sont totalement unimodulaires. Dans le cas valué, en donnant aux arêtes incidentes à s et p un coût nul, le coût total d'un couplage est le même que celui du flot associé. La recherche d'un couplage de coût maximal équivaut ainsi à celle d'un flot de coût maximal Algorithme de Ford et Fulkerson pour le flot maximal L'algorithme de Ford et Fulkerson [FF 56] permet de trouver un flot maximal dans un graphe orienté quelconque, sans coûts. Le principe est le suivant: On part d'un flot nul, et tant que le flot n'est pas maximal, on l'augmente progressivement. Pour définir la procédure qui permet d'augmenter le flot, on définit la notion de graphe d'écart. Soit flot la fonction représentant le flot partiel construit à l instant courant, on définit le graphe d écart comme étant constitué des arcs : = ij si flot(ij) < cap(ij), et on associe à cet arc ij la capacité résiduelle cap(ij) - flot(ij) = ji si flot(ij) > 0, et on associe à cet arc ji la capacité résiduelle flot(ij) On définit ensuite une chaîne augmentante comme un chemin reliant la source au puits le long d arcs non saturés (de capacité résiduelle strictement positive) dans le graphe d'écart. Soit c la plus petite capacité résiduelle parmi tous les arcs de la chaîne augmentante, on peut définir un nouveau flot en augmentant la valeur flot de c le long des arcs directs de la chaîne (les arcs ij tels que flot(ji) = 0) et en diminuant la valeur de flot de c le long des arcs indirects de la chaîne (les arcs ji tels que flot(ij) > 0). En fait, dans le cadre particulier qui nous intéresse (graphes bipartis aux arêtes de capacité 0 ou 1), à chaque étape de l'algorithme, le flot partiel est à valeur dans {0,1} et représente un couplage. Une chaîne augmentante correspond à une succession alternée d'arêtes appartenant au couplage et d'arêtes n'y appartenant pas. Augmenter le flot le long de cette chaîne revient à inverser le statut de toutes les arêtes le long de la chaîne (on fait rentrer dans le couplage les arêtes qui n'y étaient pas et on en fait sortir celles qui y étaient). Par nature, le nouvel ensemble d'arêtes est toujours un couplage. La longueur d'une chaîne augmentante dans un graphe biparti étant impaire, le nouveau couplage a une arête de plus que le précédent. Couplage initial Chaîne alternée Couplage augmenté Figure 2-1: augmentation d'un couplage par une chaîne alternée Une chaîne augmentante correspondant à un chemin de la source au puits dans le graphe d écart, on peut en trouver une par l'algorithme de recherche de plus court chemin proposé par Dijkstra [Di 59] et dont la complexité est linéaire en le nombre d'arêtes du graphe (O(m)). Cet algorithme peut en fait être considéré comme un algorithme d'optimisation locale. De tout flot, on peut arriver à un flot maximal en l'améliorant progressivement par addition de chaînes augmentantes. Le problème du flot (sans coût) fait ainsi partie de ces rares problèmes pour lesquels l'optimisation locale conduit à une solution optimale. 5
41 2.2.4 Algorithme de Busaker et Gowen pour le flot maximal de coût minimal L'algorithme de Busaker et Gowen [BG 61] est une adaptation de celui de Ford et Fulkerson au cas d'un graphe avec des coûts sur les arêtes. A chaque arête est associé un coût, et le coût total du flot est la somme, pour toutes les arêtes, du produit du flot sur l'arête par le coût unitaire de l'arête. Le principe de l'algorithme est le même : on part d'un flot nul, et que l on augmente progressivement en saturant des chaînes augmentantes dans le graphe d'écart, jusqu'à obtenir un flot maximal de coût minimal. La définition du graphe d'écart reste la même, on associe aux arcs directs le même coût unitaire que dans le graphe de départ et aux arcs indirects l'opposé du coût dans le graphe de départ. De la sorte, le coût d'un flot après une phase d'augmentation est la somme du coût initial du flot et du coût de la chaîne augmentante. Pour arriver à un coût final minimal, on ne choisit plus une chaîne augmentante quelconque, mais la chaîne augmentante de coût minimal (il s'agit donc en quelque sorte d'un algorithme glouton). En partant d'un flot nul et en l'augmentant de la sorte, on montre qu'à chaque étape de l'algorithme, le flot est de coût minimal. Ainsi, le graphe d'écart ne contient pas de cycle de coût négatif (sinon, ce cycle permettrait de construire un flot de même flux et de coût inférieur). Pour trouver ces chaînes augmentantes de coût minimal, il faut trouver un chemin de plus faible coût entre la source et le puis dans le graphe d écart. On ne peut plus utiliser l algorithme de Dijkstra car les arcs du graphe d écart sur lesquels on défait le flot ont un coût négatif. Par contre, le graphe d écart ne contenant aucun cycle de coût négatif, on peut utiliser un algorithme de parcours en largeur du graphe à l aide d une queue de priorité (fifo) en O(mn) pour trouver la chaîne augmentante : il s'agit de l'algorithm de Ford pour le plus court chemin [FF 56]. Ainsi appliqué au cas des couplages bipartis, l algorithme de flot a une complexité en O(mn 2 ) Dualité : flots et potentiels L'algorithme de Busaker et Gowen que nous venons de présenter peut en fait être amélioré grâce à une modélisation duale : le problème dual d'un flot de cout minimal est un probleme de différence de potentiel maximale. Pour un graphe orienté dont les arcs u,v sont valué par une fonction w uv., on dit qu'une valuation P sur les sommets est un potentiel si et seulement si, pour toute arc u,v, on a : P(u) + w uv P(v) Nous allons voir que le fait de raisonner avec le modèle dual des potentiels, permet, dans l'algorithme de Busaker et Gowen, de simplifier la recherche de chaînes augmentantes en se ramenenant à des coûts positifs dans le graphe d'écart. On peut alord calculer un plus court chemin dans ce graphe d'écart aux poids modifiés par l'algorithme de Dijkstra. Ceci permet de réduire la complexité de O(mn 2 ) à O(n 3 ). Le principe est le suivant : si l'on dispose d'un potentiel P sur les sommets, le plus court chemin entre deux points reste le même si l'on remplace la fonction de coût w par la fonction w' suivante : w'(u,v) = w(u,v) + P(u) - P(v) Or la valuation w' est positive. On peut donc calculer le plus court chemin entre deux points (et même d'un point à tous les autres) par l'algorithme de Dijkstra, de complexité en O(m). Les potentiels permettent ainsi de calculer les plus courts chemins, et réciproquement, les plus courts chemins permettent aussi de calculer des potentiels : en effet, on vérifie aisément que la fonction qui associe à un nœud la longueur du plus court chemin depuis la source est un potentiel. L'algorithme de Busaker et Gowen peut ainsi être adapté en intégrant le modèle des potentiels. On obtient alors un algorithme qui raisonne à la fois dans le modèle primal (flot) et dual (potentiels). L'idée est la suivante : on part d'un flot nul. On calcule une première fois les plus courts à partir de la source vers tous 6
42 les autres sommets dans le graphe d'écart en utilisant l'algorithme de Dijkstra (puisque le graphe d'écart ne contient aucun arc retour, de coût négatif), ce qui nous fournit un potentiel de départ. Ensuite, à chaque étape, on disposera d'un potentiel dans le graphe d'écart, ce qui nous permettra de calculer un plus court chemin par l'algorithme de Dijkstra (et ainsi de trouver une chaîne augmentante en O(n 2 )). Il faut donc montrer qu'en passant de l'étape k à l'étape k+1, quand on vient d'augmenter le flot le long d'un plus court chemin dans le graphe d'écart, on est capable de garder un potentiel. L'astuce de cet algorithme consiste à remarquer que les plus courts chemins à l'étape k fournissent un potentiel à l'étape k+1. Soit P la fonction de longueur du plus court chemin depuis la source à l'étape k; P est un potentiel à l'étape k, et reste un potentiel pour l'étape k+1. En effet, la seule différence entre les étapes k et k+1 (outre des changement de capacités de certains arcs, et la disparition d'autres arcs) est l'apparition de nouveaux arcs uv tels que vu soit sur le plus court chemin du graphe d'écart. Mais dans ce cas, on a (puisque vu était sur un plus court chemin), P(v) + w(v,u) = P(u) et donc P vérifie aussi la condition de potentiel pour le nouvel arc uv. Donc, P est un potentiel à l'étape k Modélisation linéaire, méthode Hongroise La méthode Hongroise est l'algorithme de choix de Recherche Opérationnelle pour résoudre les problèmes de couplages bipartis. Il s'agit en fait d'un algorithme primal-dual similaire à celui de Busaker & Gowen utilisant les potentiels, mais spécialisé au cas biparti Modèle linéaire, dualité Le problème du couplages parfait de poids minimal dans un graphe biparti peut s'exprimer de manière linéaire de la façon suivante (en notant w la fonction de poids sur les arêtes) : min w uv δ uv u,v u, v δ uv =1 v, u δ uv =1 u,v, δ uv 0 u,v, δ uv entier En fait, les équations linéaires écrites ci-dessus (qui traduisent la conservation de la matière aux noeuds du réseau) forment une matrice totalement unimodulaire, et on peut donc oublier la condition d'intégrité sur les variables δ uv. Le problème peut ainsi s'exprimer comme un programme linéaire. Le théorème de dualité nous donne alors l'égalité suivante : min w uv δ uv ο u,v ο u, v δ uv =1 et u, v δ uv =1ο ο δ uv 0 ο = ο ο ο ο max ο π u + π v ο ο ο u v ο ο ο u,v, π u +π v w uv Par rapport à la dualité précédente, ceci revient à considérer P(u) = π u pour u V 1, et P(u) = -π u pour v V 2 ). Nous allons essayer de donner une explication intuitive de la dualité en revenant à l'analogie avec 7
43 les mariages. Considérons que le couple u,v ne peut se marier que s'il a une somme d'argent strictement supérieure à w uv. Le problème primal consiste donc à marier tout le monde pour un coût total minimal. Le problème dual pourrait être le point de vue d'un philanthrope hyménophobe (!) qui veut donner des bourses aussi importantes que possible à tous ces célibataires sans qu'aucun couple ne puisse se marier. Il est alors clair que le maximum du problème dual est inférieur au minimum du problème primal. Le théorème de dualité affirme que ces deux valeurs sont égales Méthode Hongroise La méthode Hongroise, est un algorithme qui a été proposé par Kuhn [Ku 55] (voir [GM 95], p. 185). Définissons le graphe G π de la manière suivante: uv G π π u + π v = w uv. G π est ainsi constitué de l'ensemble des arêtes qui sont saturées pour le philanthrope (s'il donne à peine plus à l'homme u ou à la femme v, le couple uv se marie). L'algorithme Hongrois est basé sur la remarque suivante : les couplages de poids minimal sur G sont exactement les couplages de cardinalité maximale sur G π. On va ainsi se servir des potentiels (π) pour construire le couplage qui nous intéresse. A partir d'une solution duale réalisable, on peut construire le graphe G π, puis y chercher par l'algorithme des chaînes alternées (Ford et Fulkerson) un couplage de cardinalité maximale. La recherche d'une chaîne alternée se fait en parcourant le graphe G π en largeur d'abord. Dans le cas où ce couplage maximal est de cardinalité inférieure à n, on modifie les potentiels de manière à faire rentrer de nouvelles arêtes dans G π. Cette procédure de changement dual est la suivante. Au cours de la dernière recherche de chaîne augmentante qui a échouée dans le graphe G π, on note Even l'ensemble de sommets de V 1 qui ont été explorés et Odd l'ensemble des sommets de V 2 qui ont été explorés. A cause la structure bipartie du graphe G π, le sommets de Even ont été trouvés à profondeur paire dans l'arbre et ceux de Odd à profondeur impaire. On définit ensuite Free comme l'ensemble des sommets de V 1 n'étant pas couverts par G π. On pose alors ε = min(w uv π u π v u Even Free, v Odd) On fait ensuite la modification suivante sur les potentiels : π u + ε si u Even Free π' u = π u ε si u Odd π u sinon On peut vérifier simplement après un tel changement que si la solution duale de départ (π) était réalisable, la nouvelle (π') l'est encore, que toutes les arêtes qui étaient dans G π y sont encore et que G π contient au moins une nouvelle arête. On peut ainsi chercher à nouveau une chaîne alternée dans G π. Si l'on cherche à appliquer cet algorithme au problème de la simple recherche d'un couplage maximal, il suffit de fixer dans la formulation linéaire, pour toutes les arêtes w uv = 1. Dans ce cas, en partant d'une solution duale nulle, le premier changement dual augmente le potentiel π de tous les sommets de V 1 à 1, G π devient alors égal à G et par une suite de recherche de chaînes alternées, on trouve le couplage maximal. Ainsi, en partant d'une solution duale (π) nulle, on a au plus n changements duaux à faire, puisqu'à chaque changement, la taille du couplage augmente. Comme la complexité de la recherche de chaînes alternées est en O(m), la complexité totale de la méthode Hongroise est en O(mn). Bien que d une meilleure 8
44 complexité théorique que la version simple de l'algorithme de Busaker et Gowen (en O(mn 2 ) ), sur des petits problèmes, entre 2 20 et 2 40 noeuds, il n est toutefois plus rapide que d'un facteur 3 (pour implémentation que nous avons réalisée). 2.4 Programmation par contraintes Les problèmes d'allocation de ressources auxquels correspondent des problèmes de couplages sont traités en programmation par contraintes à l'aide de contraintes de différence. On présentera d'abord les algorithmes existants, puis on proposera des algorithmes plus fins de propagation prenant mieux en compte l'optimisation du poids du couplage La contrainte Alldifferent Si l'on se place dans le cadre des domaines finis, on peut modéliser un problème d'allocation de ressources en associant à chaque machine une variable représentant la tâche qu'elle va exécuter. Si les tâches sont représentées par des entiers, on peut imposer que toutes les variables d'allocation associées aux machines prennent des valeurs entières différentes. Plutôt que d'écrire un nombre quadratique de contraintes d'inégalités, on dispose de la contrainte globale AllDifferent [AB 91] qui spécifie que les valeurs prises par un ensemble de variables doivent être différentes deux à deux. La modélisation est ainsi différente de celle linéaire, puisque les deux ensembles V 1 et V 2 n'ont pas le même statut. V 1 est représenté par un ensemble de variables, V 2 par un ensemble de valeurs entières. Le graphe de départ est stocké dans les déclarations des domaines des variables de V 1. Deux points sont à détailler lors de la programmation d'une telle contrainte de différence. 1. les domaines des variables, qui sont souvent représentés dans les solveurs sur les domaines finis, par un intervalle d'entiers doivent ici être représentés en extension par une liste énumérée de valeurs possibles. En effet, la propagation de contraintes de différences conduit naturellement à des domaines de valeurs non connexes, et l'utilisation d'une approximation des domaines par des intervalles affaiblit grandement la propagation. 2. Il reste à préciser l'algorithme de propagation de ces contraintes Implémentation des domaines On chercher à représenter facilement des ensembles d'entiers de manière dynamique de sorte que l'on puisse facilement enlever des valeurs, ou revenir à un état antérieur. Les opérations les plus courantes sont l'itération des valeurs du domaine, le test de présence d'une valeur, ainsi que la délétion d'une valeur. Parmi les possibilités de stockage, on peut utiliser un tableau de Booléens (éventuellement sous forme de vecteur de bits, pour obtenir un codage plus compact) indiquant pour chacune des valeurs si elle est présente ou non. Le seul inconvénient de cette méthode est que l'itération requiert un temps proportionnel à la taille du tableau, c'est à dire proportionnel à la taille maximale du domaine. Nous proposons d'utiliser un codage dans un tableau implémentant une structure doublement chaînée permettant d'itérer l'ensemble des valeurs du domaine en temps proportionnel au nombre de valeurs effectivement présentes dans le domaine. La bibliothèque ECLAIR [LDJS 98] utilise ce codage pour représenter des domaines en extension. On propose d'utiliser un tableau l de taille n pour stocker un domaine Dom [1.. n]. Les entrées de ce tableau sont spécifiées de la sorte : = Si i Dom, alors l[i] = i 9
45 = Si i Dom, i-1 Dom, alors l[i-1] = j pour le plus grand j Dom, tel que j<i = Si i Dom, i+1 Dom, alors l[i+1] = j pour le plus petit j Dom, tel que j>i Cette implémentation permet d'enlever un élément du domaine en temps constant, de tester l'appartenance en temps constant, et surtout d'énumérer le domaine en temps linéaire par rapport au nombre d'éléments effectivement présents dedans Propagation des contraintes de différences La façon la plus simple de propager une contrainte globale de différence AllDifferent(X 1,..., X n ) consiste à lui associer l'ensemble des différences élémentaire X i X j, et à propager chacune d'entre elles en fowardchecking, c'est à dire d'attendre qu'une variable X i soit instanciée à une valeur v pour retirer cette valeur des domaines de toutes les autres variables X j. Nous proposons d'enrichir le problème par de nouvelles structures pour permettre une propagation plus efficace de cette containte de différence. Tout d'abord, pour rétablir une description symétrique du problème, on peut associer à chacune des n valeurs qui peuvent être prises par les n variables, un domaine inverse décrivant l'ensemble des variables pouvant prendre cette valeur. Ainsi, on peut par exemple détecter un impossibilité quand un de ces domaines inverses devient vide (c'est à dire quand une valeur n'est plus dans le domaine d'aucune variable) ou forcer une affectation quand un de ces domaines inverses est réduit à un singleton (quand une valeur ne peut plus être prise que par une seule variable, nécessairement, cette variable doit prendre cette valeur). Cette structure symétrique que l'on propose d'ajouter pour renforcer la propagation de la contrainte de différence va surtout se révéler utile pour décrire des règles de coupes liées à l'optimisation. Mentionnons enfin que si l'on ignore le problème d'optimisation, il existe une condition nécessaire et suffisante pour tester la satisfiabilité d'un problème ainsi qu'un algorithme de propagation complet. La condition nécessaire et suffisante est la suivante (théorème de la coupe minimale) : il suffit que pour tout sous-ensemble de k variables {X 1,..., X k }, l'union de leurs domaines contienne au moins k valeurs. L'algorithme de propagation complet qui permet d'éliminer toutes les valeurs qui ne participent pas à une solution est dû à Régin [Ré 94] et est basé sur une formulation en termes de flots Optimisation Que l'algorithme général d'optimisation soit fait en un seul arbre (par séparation ou évaluation) ou en plusieurs recherches, dans les deux cas, on dispose à chaque noeud d'une borne supérieure UB sur le poids maximal autorisé pour l'allocation et on va chercher à renforcer la propagation, en prenant en compte cette borne. Pour cela, nous proposons d'associer à chaque noeud x de V 1 (resp. V 2 ) le noeud best(x) de V 2 (resp. V 1 ) pour lequel l'association semble la meilleure (c'est à dire, celle qui minimise weight(x,best(x)). On peut alors estimer deux bornes inférieures du poids total de l'allocation, en considérant la somme σ += (resp. σ - ) sur les sommets x de V 1 (resp. V 2 ) des meilleures arêtes. Ces bornes permettent de couper toutes les branches de l'arbre de recherche où cette estimation est supérieure à UB. Notons que les assignations préférées (best(x)) peuvent être mises à jour de manière incrémentale, ainsi que les bornes σ + = et σ -, ce qui permet d'éviter de faire des calculs trop lourds à chaque noeud de l'arbre de recherche. Ces bornes peuvent aussi être utilisées pour faire de la propagation en éliminant certaines arêtes. Par exemple, on peut éliminer du graphe toutes les arêtes xy telles que : σ + + weight(x,y) - weight(x,best(x)) > UB 10
46 La construction de l'arbre de recherche va pouvoir tirer parti de cette structure rajoutée pour la propagation (information symétrique et choix préférés). D'une part, grâce aux domaines inverses, on va s'autoriser à poser des points de choix dans l'ensemble V 1 (les variables) aussi bien que dans l'ensemble V 2 (les valeurs). De plus, au lieu de choisir les variables de plus petit domaine, ou, comme dans un algorithme glouton, les variables x dont meilleur appariement (x,best(x)) est de poids minimal, on s intéresse à la différence de poids (appelée regret(x)) entre le meilleur choix et les autres. Le regret constitue donc la perte faite sur σ + quand on décide que best(x) ne peut être apparié à x. Choisir d'abord les noeuds de regret maximal permet d'assurer que ceux-ci seront appariés avec leur meilleur correspondant. Ce regret peut être maintenu à jour par un calcul incrémental. Enfin, on mentionnera que l'on peut affiner les bornes supérieures σ += et σ -= en prenant en compte à l'avance certains regrets. Par exemple, quand best(i) = best(j) = x, alors nécessairement, soit i, soit j ne sera pas apparié à x, et on peut donc ajouter min(regret(i),regret(j)) à σ +. On propose une borne inférieure basée sur ce type raisonnements dans [CL 97b]. Le tableau 2-1 compare l'utilité de ces algorithmes de propagation, sur trois problèmes de taille n = 20 à n = 40. On reporte pour chaque algorithme, le temps de résolution (Sparc 10) pour trouver la solution optimale et prouver son optimalité, ainsi que, le cas échéant, le nombre de backtracks lors de l'exploration de l'arbre de recherche. Mentionnons tout d'abord qu'un algorithme naïf modélisant le problème par un ensemble d'inégalités élémentaires, et construisant un arbre de recherche sur le principe du first-fail peine à résoudre des problèmes de taille On compare trois programmes, à l'algorithme de flot (Busaker and Gowen) et à l'algorithme Hongrois. Le premier de ces programmes (PPC1), utilise la représentation symétrique, les bornes σ + et σ - et le principe first-fail, le second (PPC2) utilise l'information du regret, et la version raffinée des bornes qui prend en compte à l'avance certains regrets. Le troisième programme (PPC3) utilise toutes ces techniques, mais construit un arbre binaire, en choisissant d'associer un noeud x soit à son choix préféré best(x), soit à un autre noeud, sans préciser lequel algorithme de flot 0,3 s. 0,7s. 1 s. algorithme Hongrois 0,1 s. 0,2 s. 0,3 s. PPC1 30kb. - 6 s. 200 kb s. 8 Mb min. PPC2 1 kb. - 1s. 50 kb s. 1 Mb. - 1 min. PPC3 200 b. - O.8 s. 7 kb. 10 s. 60 kb. - 3 min. Tableau 2-1: comparaison d'algorithmes pour le problème de l'allocation de ressources. En conclusion, sur le problème d'allocation de ressources pur, la P.P.C. peut être utilisée jusqu à n = 40. En deçà de 2 20 noeuds, toutes les techniques sont efficaces, une méthode naïve par contraintes doit donc être choisie pour sa simplicité, au delà de 2 40 noeuds, la méthode Hongroise doit être utilisée. 2.5 Variations Après avoir comparé trois approches différentes sur le problème du couplage parfait de poids minimal, on va maintenant s'intéresser à la résolution de variations sur le problème d'allocation avec ces techniques. On va considérer les deux variations présentées plus haut, celle prenant en compte le remplacement d'une partie du parc de machines, et celle prenant en compte deux valuations du couplage, une à optimiser, et l'autre à garder bornée. 11
47 2.5.1 Adaptation des algorithmes Les programmes par contraintes peuvent être très facilement adaptés, grâce à la séparation qu'ils opérent entre les structures de données et les principes d'inférences : il suffit généralement de rajouter quelques règles d'inférence pour passer d'un problème à une variation. C'est rarement le cas pour des algorithmes de graphes (ou pour la programmation linéaire). Une solution simple consiste alord à considérer le problème de départ comme une relaxation du problème courant et à visiter un arbre de recherche dans lequel on utilise l'algorithme de départ pour obtenir des bornes. C'est ce que nous allons faire avec les deux algorithmes de Recherche Opérationnelle considérés (couplage Hongrois et algorithme de flot). Pour ce type d'utilisation, la principale propriété de tels algorithmes est leur éventuelle incrémentalité. En effet, on va être amené, au cours de cet arbre de recherche, à résoudre de nombreuses fois des problèmes semblables. Il est donc important qu'un algorithme puisse se servir des calculs qu'il vient d'affecter au noeud père dans l'arbre de recherche pour ceux à effectuer au noeud courant, sans avoir à repartir à zéro. Les décisions d'un tel arbre de recherche vont naturellement statuer sur la présence d'arêtes dans le couplage. Quand on interdit la présence d'une arête dans un couplage, on peut facilement garder du couplage optimal précédent, toutes les arêtes sauf celle-ci, et chercher à retrouver un couplage parfait à partir de ce couplage partiel. Parmi les algorithmes présentés, l'algorithme de Ford et Fulkerson [FF 56] est incrémental, car il permet d'agrandir un flot partiel par une suite de chaînes augmentantes, jusqu'au flot maximal. L'algorithme de Busaker et Gowen [BG 61] est moins incrémental puisqu'il nécessite qu'à chaque étape, le flot courant soit de coût minimal; il ne peut donc pas compléter un flot quelconque en un flot maximal de coût minimal, à moins de commencer par transformer le flot de départ en un flot de coût minimal (en éliminant tous les cycles de coût négatif dans le graphe d'écart). La méthode Hongroise, elle, est incrémentale puisqu'elle est capable d'arriver à l'optimum à partir de n'importe qu'elle solution duale (π)=admissible. Ainsi, pour recalculer la valeur du couplage Hongrois après en avoir interdit une arête, il suffit de fixer le coût de cette arête à une valeur arbitrairement grande, ce qui la fait sortir de G π. La solution duale reste admissible. Un changement des potentiels (π) et une chaîne alternée dans G π dans permettent ensuite de recomposer le couplage Allocation de ressources avec remplacement de machines Le programme par contraintes s'adapte très facilement à cette variation. On définit le graphe d amélioration par le gain apporté lors du remplacement des vieilles machines par les nouvelles, et on ajoute aux bornes σ + et σ - la somme des k meilleures arêtes dans ce graphe d'amélioration. Ce terme peut lui aussi être maintenu incrémentalement, ce qui permet de conserver les performances de l algorithme. La méthode Hongroise s'adapte, elle aussi, convenablement à cette variation du problème. On parcourt un arbre de recherche à chaque nœud duquel on essaye toutes les affectations pour une tâche. On obtient une borne en additionnant la valeur retournée par l'algorithme Hongrois et l'estimation du grain, obtenue en sommant les k meilleures arêtes dans graphe d'amélioration. L'algorithme de flot est plus délicat à adapter. La décision au niveau i correspond au choix de la i-ème machine que l'on va remplacer. A chacun des noeuds du niveau i, on additionne la valeur du couplage optimal avec la valeur d'un couplage de cardinalité k-i dans le graphe d'amélioration. Le tableau 2-2 compare ces trois algorithmes sur trois problèmes de taille n = 20, à n = 40, pour le remplacement de k = 1,2 ou 3 machines. On donne les temps d'exécution ainsi que le nombre de backtracks dans l'exploration d'arbres de recherches. 12
48 k = 1 k = 2 k = 3 k = 1 k = 2 k = 3 k = 1 k = 2 k = 3 Contraintes 266b 0,1s 433b 0.1s. 735b 0.1s. 19kb 2.1 s. 27kb 3 s. 26kb 9.9 s. 3,3Mb 800 s. 2 Mb 400 s. 4,4Mb 700 s. Flots 5 s. 30 s. 200 s. 16 s. 70 s. 400 s. 50 s. 400 s s. Couplage Hongrois 20b. 0.1 s. 22b. 0.1 s. 31b. 0.1 s. 30b. 0.2 s. 30b. 0.2 s. 34b. 0.3 s. 40b. 0.4 s. 71b. 0.7 s. 107b. 1 s. Tableau 2-2: Comparaison d'algorithmes sur le problème de l'allocation de ressources avec remplacement de machines Ainsi, ce tableau montre que l'approche par contraintes donne de meilleurs temps que l'algorithme de flot, mais pas aussi bons que la méthode Hongroise. Le programme par contraintes est beaucoup plus rapide que la méthode Hongroise pour n = 10, un peu plus rapide pour n = 20, un peu plus lent pour n = 30, et beaucoup plus lent pour n = 40. Son principal avantage est sa simplicité. C'est donc l'approche de choix pour les petits problèmes (même pour de grandes valeurs de k), alors que la méthode Hongroise doit être préférée pour des problèmes plus grands Allocation de ressources bi-critères On dispose maintenant de deux critères pour juger d'une allocation et on cherche à maximiser l'un des deux (disons, la production) tout en gardant l'autre (disons, la consommation) borné. Ce problème est intéressant car sa difficulté dépend de l'équilibre entre la contrainte de production et celle de consommation. Si la valeur seuil de consommation est peu contraignante, cette contrainte influe peu sur le problème; si la valeur seuil est très contraignante, le problème est dominé par cette contrainte de consommation. Entre ces deux extrêmes, le problème peut être particulièrement difficile, car aucun des deux problèmes ne domine l'autre et les solutions sont alors très dispersées dans l'espace de recherche. L'algorithme de programmation par contraintes est encore une fois très facile à adapter. De même que l'on maintient deux bornes pour la fonction de production, on peut calculer deux bornes pour la fonction de consommation. Il suffit de rajouter une règle de coupe qui rejette la solution courante dès que l'estimation courante par défaut de la consommation dépasse le seuil autorisé. Les algorithmes de flot ou Hongrois sont beaucoup plus difficiles à adapter. En effet, une méthode naïve qui construit un arbre de recherche en évaluant à chaque noeud de l'arbre la valeur optimale d'un couplage du point de vue de la consommation et d'un couplage optimal du point de vue de la production est relativement inefficace. Cet algorithme, utilisant deux calculs de couplage Hongrois est reporté dans le tableau suivant, sous le nom "deux couplages Hongrois". On est donc amené à développer un algorithme de relaxation Lagrangienne qui permet de lier ces deux fonctions. Le principe général de la relaxation Lagrangienne (expliqué dans le chapitre un) consiste à modifier le problème de manière paramétrique pour intégrer une partie des contraintes dans la fonction objective. Ici, on est amené à prendre en compte la fonction suivante sur les arêtes uv : weight λ (u,v) = production(u, v) + λ(e energy(u,v)) et à résoudre de nombreux tels problèmes pour différentes valeurs de λ. On ne peut malheureusement plus tirer parti de la propriété d'incrémentalité de l'algorithme pour résoudre des problèmes paramétriques proches car la solution duale ne reste pas admissible quand on change la fonction de coût weight λ. Le tableau 2-3 propose une comparaison de quatre algorithmes sur des problèmes à n = 20 et 30 nœuds. Pour chaque problème, on considère successivement 4 instances seuils de consommation masimale successifs, du moins contraignant au plus contraignant. 13
49 Seuil de consommation Deux couplages Hongrois incrémentaux 102b 0.3 s. 2.4kb 7 s. 2.6kb 7.1 s. 276b 0.9 s. 3.2 kb 12 s. 140kb 500 s. 204kb 900 s 14kb. 60 s. Relaxation Lagrangienne avec l'algorithme de flot 20b. 21 s. 31b. 32 s. 102b. 101 s. 22b. 20 s. 107b. 242 s. 31b. 72 s. 197b. 420 s. 87b. 167 s. Relaxation Lagrangienne et méthode Hongroise 14b. 5.1 s. 64b. 16 s. 80b. 18 s. 28b. 7 s. 86b. 50 s. 28b. 12 s. 180b. 70 s. 296b. 69 s. P.P.C. 1kb. 0.4 s. 28kb. 1.2 s. 30kb. 1.3 s. 2kb. 0.1 s. 204kb 13 s. 1,5Mb 80 s. 8 Mb 410 s. 900kb 43 s. Tableau 2-3: comparaison d'algorithmes pour le problème d'allocation bi-critère De la comparaison du tableau 2-3, nous pouvons tirer plusieurs conclusions: Tout d'abord, les problèmes les plus difficiles apparaissent quand les deux critères sont du même ordre de grandeur (ce qui correspond pour nos exemples aux valeurs de seuil 550 et 900). La relaxation Lagrangienne ne porte ses fruits que pour ces problèmes difficiles, à partir d'une taille conséquente (n = 30). Comme pour l'extension envisagée au paragraphe précédent, la P.P.C. donne de meilleurs résultats qu'une extension naïve de l'algorithme de recherche opérationnelle, et même de meilleurs résultats que l'algorithme complexe de relaxation Lagrangienne, sur des problèmes de taille moyenne (n = 20). 2.6 Bilan Dans ce chapitre, nous avons proposé d'enrichir le cadre de propagation des contraintes d'allocation bijective valuées par une représentation symétrique, des calculs de bornes et un schéma de branchement combinant first-fail et regret. Nous avons montré qu'une telle approche par contraintes était limitée à la résolution de petits problèmes (jusqu'à 40 nœuds et qu'au delà, il fallait utiliser la méthode Hongroise). Nous avons envisagé le cas de deux problèmes augmentés pour lesquels les meilleures méthodes sont soit issues de l'approche simple par contraintes, soit utilisent de manière incrémentale la méthode Hongroise à l'intérieur d'une recherche arborescente. Pour mettre tous ces algorithmes à disposition d'un utilisateur d'un système de contraintes, nous proposons dans [CL 97b] de rajouter aux solveurs sur les domaines finis une nouvelle contrainte globale, une version valuée du AllDifferent, qui soit propagée en utilisant la méthode Hongroise. Cette contrainte permettrait alors de résoudre tous les problèmes d'allocation bijective considérés en mariant la souplesse de la PPC et l'efficacité de l'algorithme Hongrois incrémental. On reviendra, dans le chapitre 5, sur l'utilisation d'une telle contrainte, pour la résolution de problèmes de grilles d'emplois du temps. 14
50 15
51
52 17
53 3. Chapitre 3. Ordonnancement disjonctif Ce chapitre est consacré à la résolution de problèmes d'ordonnancement disjonctif, qui sont à la base d'un certain nombre d'applications en gestion de production industrielle. On s'intéressera au cas particulier de l'ordonnancement disjonctif, dans lequel on cherche à planifier l'exécution de tâches sur des ressources capables de ne traiter qu'une seule tâche à la fois. En résumant le travail publié dans [CL 94], [CL 95a] et [CL 95b], on proposera un cadre original de résolution en programmation par contraintes permettant de faire des inférences complexes ainsi qu'une procédure pour le développement d'arbres de recherche. On décrira ensuite d'autres techniques, issues de la Recherche Opérationnelle. Enfin, on s'intéressera à la résolution de grands problèmes, pour lesquels on proposera une heuristique gloutonne et une procédure d'optimisation locale utilisant de manière originale la propagation de contraintes. 3.1 Ordonnancement Définition, typologie, applications Un problème d'ordonnancement est défini par la donnée d'un ensemble de tâches et d'un ensemble de ressources. Les tâches doivent être planifiées dans le temps, en tenant compte des contraintes de ressource. Ainsi une tâche occupe, lors de son traitement, certaines ressources, ce qui limite les possibilités d'utilisation de celles-ci pendant cet intervalle de temps. Les problèmes d'ordonnancement se rencontrent par exemple en gestion de production où les ressources sont des machines qui fabriquent des produits et les tâches correspondent aux différents traitements de la chaîne de fabrication d'un produit. Il existe une grande variété de problèmes d'ordonnancement (voir [Got 93]). En effet, on peut envisager des ressources unitaires (capables de ne traiter qu'une seule tâche à la fois), des ressources multiples (plusieurs tâches peuvent alors être effectuées en parallèle), des ressources échangeables (certaines tâches pouvant être faites indifféremment sur un ensemble de machines), et même des ressources consommables. De plus, les machines ont parfois un état qui doit changer d une tâche à l autre (comme leur température). Il faut alors prendre en compte des temps d adaptation ( setup ) de la machine d une tâche à l autre qui dépendent de l ordre d exécution des tâches sur la machine. Au niveau des tâches, on pourra considérer des tâches de durée fixe aussi bien que des tâches préemptives (c'est à dire des tâches que l'on peut interrompre et reprendre plus tard). On envisagera un certain nombre de contraintes sur ces tâches, par exemple des contraintes de précédence, imposant que certaines tâches soient finies avant que d'autres ne puissent commencer; on pourra aussi prendre en compte des dates de fin d'exécution au plus tôt ou au plus tard. On pourra aussi contraindre les temps d'attente d'un produit entre deux tâches (par exemple, pour limiter le stockage entre deux machines). Enfin, mentionnons que du point de vue de l'optimisation, de nombreux critères peuvent être optimisés. Le plus courant, est la date de fin du projet ( makespan ). On peut aussi parfois s'intéresser à une combinaison linéaire des dates de fin par ressource, ou bien la somme des temps morts ( idle time ) par machine, ou la somme des retards par rapport à des dates de fin désirées, etc. Pour le cas qui nous intéresse, dans ce chapitre, les ressources seront unitaires, les tâches seront ininterruptibles, les relations entre tâches seront des relations de précédences quelconques et on cherchera à minimiser l'horizon de la solution (la date de fin de projet). Formellement, soit T l'ensemble des tâches, R l'ensemble des ressources et C l'ensemble des contraintes de précédence, pour t T, on désigne par d(t) la durée de t, et par use(t) la ressource sur laquelle t doit être exécutée. Si deux tâches t 1 et t 2 sont liées par une contrainte de précédence (ce que l'on notera P(t 1, t 2 )), t 2 ne peut commencer qu'après que t 1 a fini d'être exécutée. On cherche pour chaque tâche t T une date de début d'exécution start(t) telle que : 1
54 = si P(t 1, t 2 ) alors start(t 2 ) start(t 1 ) + d(t 1 ) = si use(t 1 ) = use(t 2 ) alors P(t 1, t 2 ) ou P(t 2, t 1 ) On considère ici le cas d'un graphe de précédences (la relation P) quelconque. Des cas plus restreints ont été étudiés par la communauté de Recherche Opérationnelle. Le plus célèbre de ceux-ci, l'ordonnancement d'atelier ( job-shop [Got 93]) considère des chaînes de précédences (une tâche a un unique successeur et un unique prédécesseur). L'ensemble des tâches d'une chaîne de précédence ( job ) correspond à la fabrication d'un produit. Dans ce problème, on a m machines, et n produits (jobs), chacun composé de m tâches, une par machine, à réaliser dans un ordre donné. Un cas encore plus restreint est celui du flow-shop [Got 93], où l'ordre de passage sur les machines est le même pour tous les produits (la seule différence entre les produits est leur temps de traitement sur chacune des machines) Modélisation linéaire Ce problème peut être aisément décrit dans le formalisme de la programmation linéaire en nombre entiers de la manière suivante. On associe à chaque tâche t une variable start(t) représentant sa date de début de traitement. Les contraintes de précédences sont de simples contraintes linéaires sur ces dates de début. La seule difficulté vient de l'expression des disjonctions : On utilise une technique courante de modélisation en nombres entiers qui consiste à décrire par une variable Booléenne (à valeur dans {0,1}), la branche de la disjonction que l'on considère [Ba 69]. Ainsi, pour chaque paire de tâches t,t' partageant une même ressource, on introduit la variable Y t,t' {0,1} pour représenter la contrainte disjonctive. On aura ainsi : = Y t,t' =1 si t est programmée avant t' sur leur ressource commune, = et Y t,t' = 0 sinon. Ces variables binaires sont liées par les équations suivantes : Y t,t' =1 Y t',t. Soit K une grande constante positive, la disjonction impliquant les tâches t,t' pourra être représentée par la contrainte linéaire suivante: start(t') start(t) + d(t) K.Y t',t Pour résoudre le problème par un algorithme de programmation linéaire, les contraintes d'intégrité sur les variables Y t,t' sont relaxées en 0 Y t,t' 1. Cependant, seules les solutions avec des valeurs entières sont interprétables comme des ordonnancements. Les solutions fractionnaires peuvent être utilisées directement comme bornes inférieures de l'optimum [AC 91] à l'intérieur d'un algorithme de recherche arborescente par séparation et évaluation. Ces bornes inférieures peuvent éventuellement être affinées par une procédure de relaxation Lagrangienne [vdv 91]. De manière générale, les techniques linéaires n'ont pas donné de très bons résultats sur ces problèmes d'ordonnancement disjonctif, et les algorithmes les plus performants de Recherche Opérationnelle peuvent être aisément décrits comme des algorithmes de propagation de contraintes. 3.2 Contraintes Domaines sous forme d'intervalles, PERT. La modélisation naturelle par contraintes consiste à associer à chaque tâche t une variable start(t) dont le domaine est un ensemble de dates possibles. Pour que ces domaines soient finis, on fixe une date maximale M de fin d'ordonnancement (makespan), que l on diminue à chaque solution trouvée. Gérer ces domaines de manière exacte est non seulement coûteux (il faut stocker de gros ensembles de dates, surtout dans le cas de petits pas de discrétisation du temps), et peu intéressant puisque la plupart des 2
55 règles de propagation permettent de détecter que t commence nécessairement après la date x, ou t doit être fini à la date y. On a ainsi tout intérêt à faire une approximation des domaines par des intervalles, ce qui permet de ne stocker que deux variables entières par tâche t. On définit ainsi pour t : = la date de début au plus tôt (t), et la date de fin au plus tard (t), qui définissent = la fenêtre de temps [t,t] à l'intérieur de laquelle la tâche t doit être exécutée = la quantité (t) = t t d(t) est appelée la marge (slack) de t. La propagation des contraintes de précédence se fait de manière naturelle avec ces fenêtres de temps (il s'agit de la propagation usuelle de contraintes linéaires). P(t i,t j ) t j t i + d(t i ) et t i t j d(t j ) La simple propagation des contraintes de précédence, en ignorant les contraintes de ressource, fournit des dates de début au plus tôt (ainsi que des dates de fin au plus tard), dont la donnée constitue le PERT. L'horizon du PERT est la longueur du plus long chemin dans le graphe de précédence (appelé chemin critique). Le PERT ne constitue pas une solution mais une borne inférieure qui permet d identifier des conflits de ressource sur lesquels on peut raisonner. On maintiendra dynamiquement une évaluation du PERT comme borne inférieure au cours de la résolution. On remarquera que les contraintes de précédence lient les ressources entre elles. Nous allons maintenant décrire un cadre pour raisonner sur les contraintes disjonctives associées à chacune des ressources Disjonctions. Dans le cadre habituel de programmation par contraintes, les disjonctions sont généralement peu propagées. Par exemple, la propagation en look-ahead consiste à suspendre les disjonctions (c'est à dire, les mettre en attente) jusqu'à ce que la contrainte associée à une des deux branches devienne insatisfiable, auquel cas on déclenche la propagation de la contrainte associée à l'autre branche de l'alternative. Une technique de propagation plus forte, la disjonction constructive (introduite dans [vhsd 92]), consiste à raisonner en amont de la disjonction en propageant les informations communes aux deux branches. Ces deux techniques de propagation sont en pratique trop faibles pour les problèmes d'ordonnancement et laissent de nombreuses disjonctions suspendues. On utilise généralement ces disjonctions pour la construction d'un arbre de recherche, en sélectionnant à un point de choix une disjonction inactive (φ 1 φ 2 ) et en séparant le problème en deux sous-problèmes en propageant φ 1 dans un sous problème et φ 2 dans l autre. La sélection de la paire de tâches sur laquelle on pose le choix est un des points clés de la résolution de ce problème. En effet, la taille de l arbre varie dans de grandes proportions suivant l ordre dans lequel on sélectionne les paires de tâches (cf ) Contraintes redondantes, intervalles de tâches La solution pour améliorer la propagation des contraintes de ressources consiste à rajouter des contraintes redondantes, c'est à dire des contraintes qui sont logiquement impliquées par celles qui modélisent le problème, mais qui permettent de faire plus de propagation. Une première contrainte d'intégrité, consiste à poser que pour chaque tâche t, on doit avoir : t t d(t) 3
56 Cette contrainte d'intégrité peut se généraliser en une contrainte redondante sur chaque ressource. En effet, soit T(r) l ensemble des tâches qui utilisent la ressource r, en généralisant les notations des tâches aux ensembles de tâches, on peut définir de manière naturelle : = la durée de cet ensemble d(t(r)) = d(t), t T(r) = sa date de début au plus tôt T(r) = min t T(r ) (t), = et sa date de fin au plus tard T(r) = max t T(r) (t). On remarque que la contrainte d intégrité s étend naturellement à l ensemble T(r) [vh 89] : T(r) T(r) d(t(r)) Cette contrainte redondante permet de décrire de manière globale la charge d'utilisation de la ressource par l'ensemble des tâches sans se soucier de l'ordre respectif des tâches. C'est à partir de cette contrainte redondante habituelle que nous allons décrire tout un cadre original (le formalisme des intervalles de tâches [CL 94]) pour propager les contraintes de ressources. Ce formalisme est basé sur la remarque suivante : la contrainte redondante écrite plus haut est valable pour n'importe quel ensemble de tâches T partageant une ressource r. Considérer tous ces ensembles permet de détecter de nombreuses situations irréalisables, mais génère un grand nombre de contraintes. Par exemple pour un problème d'ordonnancement d'atelier de taille n m, il faudrait considérer m (2 n -1) ensembles de tâches. L idée des intervalles de tâches est de se limiter aux ensembles maximaux vis à vis de cette contrainte, qui sont eux en nombre au plus m n 2. On définit ces ensembles comme suit : Définition: Soient t 1, t 2 deux tâches (éventuellement t 1 =t 2 ) vérifiant : use(t 1 ) = use(t 2 ) = m t 1 t 2 t 1 t 2 alors on définit l intervalle de tâches [t 1, t 2 ] comme l'ensemble suivant : [ t 1,t 2 ]= use(t) = m t 1 t t t 2 { } t 1 d(t 1 ) t 1 t 1 t 2 t 3 I=[t 2, t 3 ] = { t 1,t 2, t 3 } temps I = t 2 d(i) = d(t 1 )+d(t 2 )+d(t 3 ) I = t 3 Figure 3-1: Intervalle de tâches: I=[t 2, t 3 ] représente l ensemble {t 1, t 2, t 3 } On peut remarquer qu'à tout ensemble de tâches S={t 1,..., t i } partageant une ressource commune, on peut associer deux tâches t p et t q de S telles que S [t p, t q ]. Par construction, la contrainte d'intégrité pour 4
57 [t p, t q ] implique celle pour S. Ainsi, pour s'assurer que les contraintes d'intégrité sont vérifiées par tous les ensembles de tâches partageant la ressource, il suffit seulement de les vérifier sur les intervalles. La particularité de ces intervalles est que leur définition utilise les bornes des domaines. Il s'agit donc d'une structure dynamique : au cours de la propagation, les n 2 intervalles ne représenteront pas toujours les mêmes ensembles de tâches. La réduction d'un domaine peut provoquer l'apparition de nouveaux intervalles, leur disparition, ou encore un changement de composition dans l'ensemble de tâches associé à un intervalle. Le point important est que cet ensemble d'intervalles peut être maintenu à jour (par un algorithme incrémental en O(n 2 )) à chaque modification d une fenêtre de temps sur la ressource Propagation L information fournie par les intervalles de tâches permet non seulement de détecter des inconsistances relativement tôt (par le jeu des contraintes d'intégrité), mais aussi d affiner les fenêtres de temps des tâches par propagation. Cela se fait a l aide de deux règles de réduction, edge finding et exclusion qui, toutes les deux, permettent de détecter la position relative d une tâche par rapport à un ensemble d'autres tâches. On va décrire ces règles de déduction par des formules logiques, et on évoquera l'implémentation de cette propagation un peu plus loin Edge finding Cette règle permet de détecter qu une tâche ne peut pas être effectuée en premier (avant toutes les autres tâches) dans un intervalle. Cette règle raisonne sur les extrémités de l intervalle, et permet de découvrir de nouvelles relations de précédences (ou de nouvelles arêtes dans le graphe d'ordre des tâches, ce qui explique son nom. Cette règle s'applique aussi bien aux tâches à l'intérieur de l'intervalle qu'à celles qui sont à l'extérieur (parce qu'elles peuvent soit commencer plus tôt, soit finir plus tard). R (t S S t d(s) < 0) t, S, R R (t S S t d(s) d(t) < 0) R t 0 S {t} tq. P(t 0,t) Règle edge-finding On remarque que la conclusion est une formule quantifiée existentiellement sur un ensemble fini. Cette conclusion peut être propagée de plusieurs manières. On peut par exemple créer dynamiquement une disjonction de contraintes de précédences, mais cette méthode est coûteuse en mémoire. On peut aussi procéder à la mise à jour suivante des fenêtres de temps : t 0 S {t} tq P(t 0,t) t min{ t i + d(t i ),t i S {t} } Propagation naïve de la règle d'edge-finding) On peut cependant être beaucoup plus précis. Soit D = d(s {t}) (S t), alors en cas de déclenchement de la règle, nécessairement, D > 0. Comme t ne peut pas être exécutée avant toutes les tâches de S, il existe un sous ensemble V de S-{t} tel que t soit exécutée après V et avant S-{t}-V. On peut alors affirmer que la durée de V est au moins D. Une mise à jour beaucoup plus fine consiste alors à poser t 0 S {t} tq P(t 0,t) { } ( earliest _ end(v) ) t min V V S {t} tels que d(v ) D et V +d(s {t}) S {t} V Propagation forte de la règle d'edge-finding 5
58 en prenant earliest _ end(v) = max W V (W + d(w)). Calculer cette mise à jour de façon exacte est beaucoup trop long (cela revient à la résolution exacte d'un problème de sac à dos), mais la mise à jour naïve est trop peu précise: il est donc important de trouver une approximation qui soit à la fois précise et rapide à calculer, comme celui proposé dans [CL94], [CL95b] Exclusion La deuxième règle de propagation permet de détecter qu une tâche t sera exécutée après tout un intervalle S (t S). On peut la déclencher dans deux cas: t, S, R R t S, S t d(s) d(t) < 0, R packed(s,t) after _each(t,s) R Règle d'exclusion t i S, P(t i,t) R R t S + d(s) R t i S,t i t d(t) Les deux situations détectées sont les suivantes: packed(s,t) S S < d(s) + d(t) faire rentrer t. ( ) correspond au cas où la fenêtre de S est trop tendue pour y ( ) correspond au cas où t est après chaque tâche dans S, pour une raison soit due aux précédences, soit due aux fenêtres de temps. after _ each(t,s) t i S, P(t i,t) t > t i d(t i ) Implémentation des règles de propagation Le cadre que nous proposons avec les intervalles de tâches implémente la propagation sur les domaines de tâches par des règles. Ceci est possible d'une part grâce à l'algorithme de maintenance incrémental des intervalles et d'autre part, grâce au compilateur de règles logique Marie [Ca 87], inclus dans Claire, qui nous permet de programmer les règles en décrivant les conditions de déclenchements telles qu'elles sont décrites ici. Si l'on souhaite implémenter un tel algorithme dans un langage impératif, deux directions sont possibles : = on peut garder la structure des intervalles de tâches et faire le travail du compilateur de règles à la main, c'est à dire trouver, pour chacune des conditions intervenant dans les règles toutes les modalités de déclenchement possibles. Ceci revient à programmer des démons surveillant les bornes de fenêtres de temps. = on peut aussi se passer de la structure dynamique des intervalles de tâches et chercher un algorithme impératif qui examine l'ensemble des tâches associées à une ressource pour y faire toutes les propagations possibles. La première approche est plus réactive, alors que la seconde implémente un mode de propagation plus global. De nombreux algorithmes de propagation ont été proposés, par Carlier & Pinson [CP 94], Nuijten [Nu 94], Martin & Shmoys [MS 96]. Leur complexité est généralement meilleure que celle des intervalles tâches (O(n log(n)) pour l'algorithme de Carlier & Pinson contre O(n 3 ) pour les intervalles de tâches), mais leur déductions sont parfois moins puissantes. On renvoie le lecteur à [BL 95] pour une comparaison des principaux algorithmes de propagation. En 1995 [CL95b], ces règles nous ont permis de résoudre un problème ouvert depuis 1984 (le jobshop de taille LA21 [La 84]) et d'améliorer des bornes inférieures sur deux problèmes ouverts de taille (YAM1 et YAM2 [NY 92]). En pratique, 6
59 aujourd'hui, la plupart des algorithmes de propagation sur une ressource disjonctive sont d'une efficacité comparable sur les problèmes de job-shop de taille inférieure à Techniques de Recherche Opérationnelle On décrit dans cette section un certain nombre de techniques algorithmiques complémentaires du cadre de propagation de contraintes décrit précédemment. On commence par mentionner quelques techniques de coupe différentes, on aborde ensuite la construction d'arbres de recherche; enfin, on décrit une technique originale qui utilise un parcours en largeur limité dans un arbre de recherche pour renforcer la propagation Autres coupes, relaxation préemptive Comme on l'a indiqué plus haut, la plupart des algorithmes exacts de résolution de problèmes d ordonnancement construisent un arbre de recherche et propagent ces règles (ou un sous ensemble), pour déterminer si une tache doit, peut ou ne peut pas être la première (resp. la dernière) d un intervalle de tâches. Trois exceptions notables utilisent des techniques différentes : La relaxation préemptive. On parle d ordonnancement préemptif quand les tâches peuvent être interrompues. Il s'agit donc d'une relaxation du problème envisagé, qui peut fournir des bornes inférieures. Le problème préemptif multi-machines est NP-complet et peu étudié [BL 96b]. Le problème mono-machine avec fenêtres de temps peut être résolu en temps linéaire par l algorithme de Jackson [Got 93], qui exécute les tâches par date de fin au plus tard croissantes (dès qu une tâche plus prioritaire devient disponible, la tâche en cours est interrompue et on commence la nouvelle). Cet algorithme fournit un ordre et une borne inférieure pour chaque ressource, qui peuvent servir à guider et borner la recherche. La valeur de l ordonnancement préemptif sur une machine est reliée de la façon suivante aux intervalles [CP 96]: pour un ensemble de fenêtres de temps données, ( ) Preempt(r) = min S T(r) S + d(s) + (M S) cette borne inférieure a donc exactement le même pouvoir de coupe que la contrainte d intégrité sur les intervalles. Le raisonnement énergétique [Lo 91], [ELT 91]. On définit l'énergie d'une tâche t pour une fenêtre de temps [a,b] comme la durée minimale de t qui sera faite entre les dates a et b. On peut comptabiliser, pour une fenêtre de temps donnée, l ensemble des contributions énergétiques et vérifier que leur somme est inférieure à la largeur de la fenêtre (b-a), ce qui fournit une nouvelle contrainte d'intégrité par intervalle temporel. En considérant comme fenêtres de temps celles associées à des intervalles de tâches, on peut obtenir un cadre de propagation un peu plus riche. Cependant, on ne connaît toujours pas d algorithme efficace pour maintenir cette structure supplémentaire de manière incrémentale, ce qui rend l analyse énergétique peu compétitive par rapport aux technique classiques d'edge-finding. la modélisation par séquence, proposée par Zhou [Zh 97] considère l'ordonnancement sur une ressource comme une séquence à l'intérieur de laquelle chaque tâche est à une place donnée. Si l'on décrit ces places dans la séquence par des variables de domaines, on peut les contraindre par une contrainte AllDifferent, et par des règles spécifiques faisant le lien entre le modèle de séquence et le modèle temporel. Cette modélisation a fourni de bons résultats sur certains problèmes. 7
60 3.3.2 Schéma de branchement Les règles de propagation sont utilisées à chaque nœud d'une recherche arborescente. La stratégie de branchement classique proposée par Carlier et Pinson s appelle aussi edge-finding et construit l ordonnancement par le début et la fin: on choisit, pour une machine, la première, puis la seconde, etc... (resp. la dernière,...) des tâches exécutées (cette stratégie est en quelque sorte basée sur le modèle séquentiel évoqué plus haut). On commence généralement par ordonnancer la machine la plus tendue (celle de plus faible marge (T(r)) ). Ceci revient à examiner les goulots d étranglement (bottlenecks) d abord. La structure des intervalles de tâches permet de faire ce raisonnement de manière plus fine en s intéressant à des intervalles plutôt qu à des ressources entières. On décrit dans les deux paragraphes qui suivent le résultat d'une comparaison expérimentale de l'efficacité de différentes procédures de branchement. Ce point est extrêmement important. Les heuristiques de sélection du point de choix sont rarement décrites dans la littérature, alors que les algorithmes de recherche arborescente peuvent être extrêmement sensibles à cet aspect de leur construction. En effet, pour des problèmes comme l'ordonnancement, où les étapes de propagation à chaque nœud peuvent être conséquentes en temps de calcul (par rapport à des problèmes de propagation de contraintes plus simples), le temps de sélection du point de choix est négligeable par rapport à la propagation et on a tout intérêt à choisir ce point de choix au mieux. Le schéma de branchement d edge-finding peut aussi être adapté de deux manières différentes : plutôt que de choisir, pour une machine, la première ou la dernière tâche, on préfère ordonner entre elles deux tâches susceptibles d être exécutées en premier ou en dernier dans un intervalle, ce qui permet d avoir un arbre binaire dont chacune des deux branches propage beaucoup d information. Il s'agit donc d'une procédure de branchement qui sélectionne une disjonction suspendue pour en propager chacune des deux branches et cette procédure utilise le modèle des intervalles de tâches pour sélectionner la disjonction à examiner. Suivant le principe first-fail, on va choisir un intervalle pour lequel le nombre de candidats à être première/dernière tâche est relativement faible (on consacrera ainsi peu de noeuds à l'examen de cet intervalle). Enfin, on procède à une évaluation entropique des points de choix possibles: pour chacune des branches, en essayant d'évaluer à l avance la réduction des fenêtres de temps qui sera causée par la décision de la branche. L idée est de sélectionner un point de choix qui entraîne une forte réduction du nombre de degrés de liberté du système en restreignant le plus possible les domaines (pour limiter autant que possible la profondeur de l arbre). Les choix dont on peut espérer qu ils auront le plus d impact sur l'ensemble des fenêtres sont a priori ceux qui réduisent fortement des fenêtres, dans une situation déjà assez tendue. Après de nombreuses expérimentations, la procédure qui s'est avérée la meilleure consiste à sélectionner les paires de tâches (t,t') en minimisant, pour chacune des tâches t et t' la fonction suivante: f (,δ ) = ( δ)2 où représente la marge d une fenêtre de temps (soit t t d(t) pour la tâche t) et δ la variation attendue de cette marge suite à la décision de la branche (soit, pour la tâche t la quantité t min(t,t' d(t')) dans la branche où t est ordonnancé avant t' et la quantité max(t,t' + d(t')) t dans l'autre branche). En résumé, la procédure complète prend en compte trois critères pour sélectionner le noeud de branchement: l évaluation entropique des décisions, le first-fail et les goulots d étranglements. L'ensemble de ces ingrédients permet de faire la preuve d optimalité du célèbre problème MT10 (un jobshop de taille proposé dans [MT63]) en 1500 backtracks. Ce chiffre peut monter rapidement à plusieurs dizaine de milliers avec une heuristique imprécise et à des millions de backtracks si on n utilise pas d edge finding). Le tableau 3.1 résume l'utilisation de algorithme complet de programmation par 8
61 contraintes (preuve d'optimalité) sur une série de dix instances difficiles de job-shop de taille (proposées dans [MT63], [La84], [ABZ88] et [AC 91]). Problème backtracks temps Problème backtracks temps MT b. 106 s. ORB b. 550 s. ABZ b. 85 s. ORB2 456 b. 36 s. ABZ6 157 b. 9,9 s. ORB b. 340 s. LA b. 63 s. ORB b. 82 s. LA b. 52 s. ORB5 799 b. 61 s. Tableau 3-1: Résolution de jobshop de taille en P.P.C. (Sparc 10, [CL 95b]) Shaving Cette partie décrit une technique basée sur la recherche arborescente pour renforcer la propagation. Comme nous l avons vu, la propagation et l arbre de recherche se font par deux modélisations différentes: la propagation est faite dans un modèle temporel, alors que le branchement est fait dans un modèle d ordre relatif des tâches. Le modèle temporel peut cependant être utilisé pour affecter des dates d'exécution aux tâches et faire, de manière limitée, une exploration en largeur de cet l arbre de recherche. Concrètement, cela revient à faire démarrer une tâche t au plus tôt (resp. au plus tard) dans sa fenêtre de temps en posant start(t) = t et à propager les conséquences de ce choix. Si une inconsistance est détectée, alors on sait que t ne peut commencer à la date t, on peut donc retirer t du domaine de start(t), ce qui revient à incrémenter t. Cette technique de réduction des domaines par une exploration limitée en largeur s appelle shaving et a été introduite dans [CP 94], puis développée par une implémentation très efficace dans [MS 96]. Pour obtenir une procédure de shaving efficace, l'implémentation doit être très soignée. Parmi les améliorations possibles, on peut essayer de réduire les domaines de quantités variables: si une réduction d une unité réussit, on essaye de réduire de 2, puis de 2 2, etc... L ordre dans lequel on examine les tâches doit aussi tenir compte des contraintes de précédence. Enfin, cette technique nécessite un schéma de propagation rapide. Les intervalles de tâches (en O(n 3 )) sont une technologie trop lourde, un algorithme en O(n 2 ) est nécessaire, comme celui de Carlier & Pinson, de Nuijten ou de Martin & Shmoys. Le shaving est une technique lourde qui utilise beaucoup la propagation, aussi préfère-t-on pour cette dernière la rapidité à la puissance. Dans le même ordre d'idées, on peut aussi considérer du shaving récursif (correspondant à une exploration en largeur de l arbre, limitée à profondeur 2), mais celui-ci est beaucoup trop lent pour le gain qu'il apporte par rapport au shaving simple. Le shaving peut être effectué avant toute recherche ou à chaque noeud de l arbre de recherche. De manière surprenante, il ne s avère utile que quand on l utilise à chaque noeud: Il permet alors de réduire considérablement la taille des arbres. D'un point de vue expérimental, cette technique donne des informations de qualité remarquable sur la faisabilité d un problème; en revanche, la manière dont les domaines sont réduits semble relativement peu exploitable pour le reste de la recherche (en particulier, il pollue les domaines pour l'évaluation entropique: les marges des fenêtres deviennent moins représentatives de la difficulté d une situation). Ainsi que le relate le tableau 3-2, le comportement du shaving sur le problèmes est assez instable: le gain en nombre de backtracks est systématique, mais ne se traduit pas toujours par un gain en temps (on a même une perte de temps sensible pour le problème ORB1). En revanche, l amélioration en temps est nette sur certains problèmes difficiles (d'un facteur 10 sur le problème LA21). 9
62 problème taille sans shaving avec shaving MT10 10 = s 1575 bk 107 s 7 bk ORB1 10 = s 7265 bk 780 s 99 bk ORB2 10 =10 36 s 456 bk 46 s 6 bk ORB3 10 = s 4323 bk 172 s 51 bk LA21 15 =10 36 heures 2 Mbk 4 h. 400 bk Tableau 3-2: Ordonnancement disjonctif avec et sans "shaving" (Preuves d optimalité sur une SPARC-10) 3.4 Solution heuristique et optimisation locale Après avoir décrit des approches exhaustives permettant une résolution exacte des problèmes d'ordonnancement disjonctif, nous allons maintenant proposer des algorithmes permettant une résolution approchée du problème. Ces méthodes peuvent être intéressantes dans deux contextes : d'une part pour la résolution de problèmes de grande taille, d'autre part pour la résolution en temps limité Algorithmes gloutons et règles de sélection La plupart des procédures heuristiques pour construire un ordonnancement sont des heuristiques gloutonnes de sélection par règle de priorité (aussi appelés algorithmes de liste). Ces procédures construisent l'ordonnancement de manière chronologique, en parallèle sur toutes les ressources. Quand tout l'ordonnancement a été construit jusqu'à la date t 0, on déroule mentalement l'exécution du plan jusqu'au temps t 0, on considère l'ensemble des tâches qui peuvent commencer de manière immédiate, et on en choisit une d'entre elles que l'on fait commencer immédiatement. On répète ce procédé tant qu'il reste une machine et une disponible au temps t 0. On fait ensuite progresser le temps jusqu'au premier instant t 1 auquel une machine est à nouveau accessible et une tâche est disponible. On planifie de la sorte toutes les tâches une par une. Il y a ainsi autant d heuristiques que de règles de sélection de la tâche parmi celles qui sont disponibles. Les règles de sélection se classent en deux familles: les règles statiques qui ne dépendent que des données de la tâche (durée, graphe de précédence) et les règles dynamiques qui dépendent de l ordonnancement partiel construit : Parmi les règles statiques, citons les critères SPT (shortest processing time), LPT ou MWKR (most work remaining). Remarquons que l algorithme de Jackson correspond à un algorithme de sélection préemptif mono-machine pour MWKR. (dès qu une tâche plus prioritaire devient disponible, on arrête l exécution de celle en cours). Les règles statiques donnent en général des résultats instables : par exemple, sur les 40 problèmes de Lawrence, 8 des 10 règles considérées sont meilleures que les 9 autres sur au moins un des 40 problèmes [Law 84]. De plus, les ordonnancements construits sont le plus souvent de piètre qualité. les règles dynamiques tiennent compte de l ordonnancement construit jusqu à présent. On peut, par exemple, chercher à placer les tâches qui génèrent le moins de temps morts dans l ordonnancement. Une règle qui donne de très bons résultats est GREEDY, règle gloutonne qui évalue une borne inférieure de l ordonnancement en cours et cherche à placer la tâche qui augmentera le moins possible cette borne inférieure. Les résultats varient légèrement suivant la borne inférieure considérée ([DA 93], [CL 95a]), mais ces résultats sont stables et systématiquement de bonne qualité. 10
63 Ainsi, sur les problèmes difficiles (comme MT10 ou la série ORB), les règles statiques n arrivent pas à moins de 20% de l optimum, quand GREEDY fournit des solutions à moins de 10%. Sur les problèmes plus grands, l écart est encore plus flagrant. Souvent, plusieurs tâches ne peuvent être départagées par un seul critère, on peut alors modifier la procédure de plusieurs manières: soit en considérant plusieurs critères; Par exemple on sélectionnera d abord sur GREEDY, puis sur MWKR, enfin sur LPT. On peut aussi randomiser la procédure et lancer plusieurs exécutions et prendre la meilleure des solutions. Enfin, on peut encore construire la solution par des règles de sélection de chaque coté de l ordonnancement, en le construisant par le début et par la fin. Problème taille optimum SPT GREEDY MT MT LA LA LA Tableau 3-3: Comparaison d'heuristiques (règles de sélection) pour l'ordonnancement disjonctif Échanges, réparations, marches aléatoires On s'intéresse enfin aux procédures d'optimisation locale permettant d'améliorer une première solution fournie par une heuristique. Plusieurs algorithmes d optimisation locale ont été proposés pour le job-shop. La plupart d entre eux considèrent comme voisinage d une solution l ensemble des solutions obtenues par interversion de deux taches exécutées de manière consécutive sur une même machine. Cette procédure peut être améliorée si l on remarque que seules les paires de tâches situées sur des chemins critiques sont susceptibles de faire diminuer la date de fin de l ordonnancement. En repérant à l avance les paires de tâches intervertibles situées sur tous les chemins critiques, on peut trouver des permutations qui vont nécessairement améliorer la solution. Un algorithme de descente basé sur ces voisinages fournit ainsi une procédure de réparation. qui permet d'améliorer de manière déterministe la solution. Cependant, cette structure de voisinages est très locale et comporte énormément de minima locaux (en permutant deux tâches à partir d une solution raisonnable, on a toutes les chances d abîmer la solution). On ne peut donc pas se contenter de faire de l optimisation locale déterministe avec elle: soit il faut randomiser la procédure, soit il faut considérer d autres voisinages. Parmi les essais de randomisation de cette procédure, les procédures de recuit simulé donnent de piètres résultats [vl 92], alors que certains tabous sont excellents [NS 93]. Mentionnons ici le fait que les tentatives d algorithmes génétiques n ont pas encore fait leurs preuves à cause de la difficulté de croiser deux solutions Shuffle et shuffle étendu Une autre direction consiste à explorer des voisinages plus grands. On dira ainsi que deux solutions sont voisines si les tâches sur une ou plusieurs machines sont effectuées dans le même ordre pour les deux solutions. L exploration de tels voisinages s appelle le shuffle et est issue des travaux de [ABZ 88] et [AC 91]. Nous proposons une généralisation de ces méthodes d'optimisation locale [CL 95a]. Ces voisinages contiennent énormément de solutions et il n est pas réaliste de les explorer de manière 11
64 exhaustive. Puisqu'on envisage des voisinages de grande taille, on peut se contenter d'appliquer une méthode de descente et restreindre l'exploration de ces voisinages aux solutions qui améliorent la fonction objective. Or, on sait de manière expérimentale que l algorithme de résolution, l edge-finder est particulièrement rapide à recomposer une solution dans des situations tendues (grâce aux règles de propagation et à l analyse entropique). On choisit donc de figer la solution (l ordre relatif des tâches) sur une machine et d oublier tout ordre sur les autres; un arbre de recherche est alors exploré à partir de cette solution partielle pour retrouver une autre solution au moins aussi bonne que la précédente. Tant que l on est relativement loin de l optimum, on a intérêt, pour accélérer la convergence, à forcer l edge-finder à améliorer la solution précédente (en diminuant l horizon autorisé M, par rapport à la solution précédente) ainsi qu à ne lui accorder qu un nombre très faible de backtracks. En effet, rien ne garantit qu il existe une solution sensiblement meilleure dans le voisinage considéré, on ne veut donc pas perdre trop de temps dans une exploration qui risquerait d être vaine. Au fur et à mesure de la recherche, le pas d amélioration (la quantité dont on force M à diminuer) peut être baissé (il commence généralement à 1% de l horizon pour finir à une unité de temps), le nombre de backtracks autorisés peut augmenter (d une dizaine à quelques centaines ou quelques milliers), et le nombre de machines sur lesquelles on fige l ordre diminuer (de 2 à 1 pour les 15 10) pour agrandir les voisinages. Ainsi, quand on est loin de l optimum et que l espace est dense en solutions, on force la marche vers l optimum, et on autorise que de très petits arbres (soit le problème est insatisfiable, soit il est a priori dense en solutions auquel cas on peut en trouver une très rapidement), alors que quand on s approche de l optimum, l amélioration forcée est moins brutale (pour éviter que le problème ne soit systématiquement infaisable) et les arbres parcourus sont plus gros (car les problèmes sont a priori moins denses en solutions). L idée de ne garder d une solution que l ordre sur une machine est parfaitement arbitraire, on explore donc aussi d autres voisinages (shuffle étendu [CL 95a]) : on peut par exemple figer l ordre respectif de toutes les tâches dont l exécution est comprise entre deux dates, ou figer l ordre relatif des tâches sur une ressource, sauf pour les tâches sur un chemin critique. L idée de cette phase d optimisation locale est d explorer de manière concurrente des voisinages très différents pour profiter de tous les mouvements faciles (détectés en peu de backtracks) de chaque voisinage. Le tableau 3-4 est basé sur une comparaison effectuée en 1994 [VAL 94] entre un grand nombre de techniques aléatoires pour le jobshop sur un jeu de 13 problèmes difficiles de taille 5 10 à (à l'époque de cette comparaison, certains problèmes étaient encore ouverts, c'est à dire que l'on ne disposait que d'un encadrement de la valeur de l'optimum). En 1995, nous avons évaluée notre technique de shuffle sur ces mêmes problèmes. Le shuffle étendu arrivait ainsi en 1995 ex-aequo avec le meilleur tabou [NS 93] (0,36%), mais au prix de temps d éxécution plus importants. Méthode distance moyenne à l'optimum (sur 13 problèmes difficiles) recherche tabou [NS 93] 0.36 % shuffle étendu [CL 95a] 0.36 % recherche tabou [DT 93] 0.83 % recherche tabou [BaC 94] 1.38 % shuffle [AC 91] 1.96 % recuit simulé [vl 92] 2.08 % algo. génétique [DP 94] 3.31 % 12
65 Tableau 3-4: Comparaison de méthodes approchées pour la résolution d'instances difficiles de jobshop (à partir de [VAL 94]) Comme illustration globale de l'ensemble des techniques présentées dans ce chapitre, on décrit ([CL95b]) un algorithme complet, calculant une première solution avec l'heuristique gloutonne, puis l'améliorant avec la procédure de shuffle étendu, puis terminant la recherche par l'exploration d'un arbre en branch&bound. Cet algorithme calcule aussi deux bornes inférieures, l'une par simple propagation, l'autre, en ordonnant une seule des machines. Le tableau 3-5 illustre le comportement de cet algorithme sur deux problèmes, MT10 (jobshop [MT 63]) et LA21 (jobshop [La 84], ouvert jusqu'en 1995). MT10 (optimum=930) LA21 (optimum=1046) borne inférieure (simple propagation) s s. borne inférieure (ordonnance une machine) s s. solution initiale (glouton) optimisation locale (shuffle étendu) s s. preuve d'optimalité 1575 b. 80s. 1 Mb. 24h. Procédure complète 150 s. 24 h. Tableau 3-5: Procédure complète de résolution sur deux instances de job-shop (PC Pentium 90, [CL95b]) 3.5 Bilan Aujourd hui, tous les problèmes difficiles sont résolus en quelques milliers de backtracks et quelques minutes. Les problèmes ouverts que l'on cherche à résoudre de manière exacte sont de taille entre et Pour ceux-ci les méthodes de branch & bound avec propagation de contraintes surpassent de loin les méthodes de programmation linéaire en nombres entiers. De plus, les bonnes heuristiques sont basées sur des règles de sélection dynamiques, et les meilleurs schémas d optimisation locale sont les tabous et le shuffle étendu. Nous avons proposé un cadre pour propager les contraintes de ressource, une heuristique gloutonne basée sur l'évaluation à l'avance d'une borne inférieure utilisant de la propagation de contraintes, ainsi qu'une procédure d'optimisation locale utilisant la recherche arborescente et la propagation de contraintes pour explorer un voisinage. Il y a quatre ans, le problème référence au sein de la communauté des contraintes était le problème d'ordonnancement du pont (un problème avec 7 ressources et une cinquantaine de tâches [vh 89]), dont la résolution nécessitait de quelques centaines à quelques milliers de backtracks. Depuis, les solveurs de contraintes ont intégré au niveau de la propagation des techniques d edge finding comme la contrainte cumulative ou les intervalles de tâches. Aujourd hui, des programmes par contraintes peuvent rivaliser avec les techniques d algorithmique traditionnelle (le problème LA21 a été résolu en 1995 par trois équipes dont deux utilisaient des contraintes). Une des leçons pour la communauté de la P.P.C. aura été que ces mécanismes de propagation dédiés aux problèmes d ordonnancement ne peuvent pas être remplacés par des techniques génériques comme la 13
66 disjonction constructive (en 1994, il fallait 90 heures pour résoudre MT10 en cc(fd) [vhsd 93] avec la disjonction constructive contre 6 minutes en Laure avec les intervalles de tâches [CL 94]). 14
67 15
68 4. Chapitre 4. Le voyageur de commerce (TSP) Le problème du voyageur de commerce (ou TSP pour Traveling Salesman Problem) est le suivant : un représentant de commerce peut vendre sa marchandise dans un certain nombre de villes, il doit donc planifier sa tournée de manière à passer par toutes les villes en voyageant au total le moins possible. Ce problème est sans doute un des plus vieux et problèmes d'optimisation combinatoire et certainement l'un des plus étudiés. Les applications du problème du voyageur de commerce sont nombreuses : D'une part, certains problèmes d'optimisation de parcours en robotique ou en conception de circuits électroniques ainsi que certains problèmes de séquencement (passage de trains sur une voie, atterrissage d'avions, processus de fabrication en industrie chimique, etc.) s'expriment directement sous forme de TSP. D'autre part, et c'est sans doute la famille d'application la plus importante, les problèmes de transport abordés au chapitre 7, sont généralement plus complexes que le TSP, mais comportent des sous-problèmes de type TSP. L'étude du problème du voyageur de commerce est donc un préalable à la résolution de ces problèmes de transport. On présentera un rapide tour d'horizon des méthode disponibles pour la résolution du TSP et, à partir de [CL 97a], on montrera comment résoudre de manière efficace des TSP de taille inférieure à 70 nœuds. 4.1 Circuits Hamiltoniens Définition, typologie, applications. Soit G = (V,E) un graphe, et soit d une fonction positive sur les arêtes, on appelle circuit Hamiltonien un chemin fermé passant par tous les sommets v V. De manière duale, on appelle circuit Eulérien un chemin fermé passant par toutes les arêtes e E. Le problème du voyageur de commerce revient ainsi à trouver un circuit Hamiltonien de longueur minimale dans le graphe de distance des villes. Bien que les définitions des circuits Eulériens et Hamiltoniens soient très proches, ces deux problèmes sont de difficultés très différentes. Le simple problème de l'existence d'un circuit est NP-complet pour les circuits Hamiltoniens et polynomial pour les circuits Eulériens. En effet, il existe un circuit Eulérien si et seulement si le graphe est connexe et tous les sommets sont de degré pair (théorème d Euler). Dans ce cas, la recherche d'un tel circuit peut se faire avec un algorithme de complexité O(m) [Sch 95] (on rappelle que m = E désigne le nombre d'arêtes de G). Pour le problème de minimisation de parcours, la fonction d peut être quelconque. Des algorithmes spécifiques peuvent être proposés quand il s'agit d'une distance L 1, L 2 ou L, (par exemple, en procédant à une triangulation de Delaunay de l'espace géographique sous-jacent), ou simplement quand la fonction d vérifie l'inégalité triangulaire. Enfin, la matrice de distance peut aussi être asymétrique (par exemple, si l'on considère le cas d'une ville avec des sens uniques). La cas particulier du TSP asymétrique (ou ATSP) est particulier puisque ce problème est en fait beaucoup plus facile que le TSP symétrique. L'algorithme que nous proposerons s'appliquera aussi bien au TSP asymétriques que symétriques. Enfin, ce problème du voyageur peut être envisagé avec des contraintes supplémentaires. Les plus courantes viennent du domaine du transport et sont liées soit à une formulation temporelle, soit à des problèmes de capacité. = Dans la métaphore habituelle du voyageur de commerce, la fonction d représente une distance géographique. Elle peut tout aussi bien représenter un temps de trajet. Si l'on particularise une ville comme ville de départ (appelée le dépot), d'où le tour commence au temps 0, on peut associer une heure de visite à chaque ville. Dans cette description temporelle du problème, on peut considérer deux types de contraintes supplémentaires. On peut contraindre les dates de visite en associant à chaque ville une fenêtre de temps pendant laquelle le tour doit y passer. Le problème est alors dénoté par les 1
69 initiales TSPTW (Traveling Salesman Problem with Time Windows). D'autres contraintes peuvent aussi faire dépendre le temps de trajet entre deux endroits (c'est à dire la distance d) de l'heure à laquelle le trajet est effectué. Cette formulation rend bien compte des situations d'embouteillages autour des grandes villes le matin et le soir. = Si l'on considère que le voyageur de commerce est en fait un transporteur qui peut à chaque endroit prendre et déposer de la marchandise (les pickup and delivery problems), on est amené à rajouter deux types de contraintes. D'une part des contraintes de précédences (aller chercher la marchandise avant de la livrer), et d'autre part des contraintes de capacité vérifiant que l'on est bien physiquement capable de transporter toutes les marchandises. Ceci peut éventuellement interdire des trajets qui commenceraient par procéder à tous les enlèvements avant de faire la moindre livraison. On traitera dans ce chapitre de la résolution du TSP pur et de la prise en compte d'éventuelles fenêtres de temps. Le TSPTW est en effet particulièrement intéressant puisqu'il est à mi-chemin entre un TSP et un problème d'ordonnancement disjonctif. Si toutes les fenêtres de temps sont égales à [0, M], on obtient le TSP pur, et si les distances d ij ne dépendent que de i (ce qui peut correspondre à un temps de visite au noeud i et un temps de trajet nul entre i et j), on retrouve un job-shop à une machine. Il y a donc un continuum entre le TSP auquel on ajoute des fenêtres de temps et l'ordonnancement disjonctif auquel on ajoute des temps d'adaptation de la ressource entre le traitement de deux tâches successives. Mentionnons enfin qu il existe de nombreuses autres variantes du TSPTW, suivant ce que l on cherche à optimiser : au lieu de chercher à minimiser la date de fin, on peut vouloir minimiser une distance géographique (on n additionne que les temps de parcours et on ignore les temps d attente), ou bien minimiser la somme des temps d attente. On peut aussi autoriser le tour à ne pas commencer en 0 au dépôt, mais un peu plus tard afin de pouvoir réduire certains temps d attente intermédiaires Modélisation linéaire. La formulation linéaire classique du problème est la suivante: on associe à chaque arête e du graphe une variable binaire x e reflétant la présence de e dans le tour. On cherche alors : min d ij x ij i, j i, x ij =1, j, x j ij =1 i S V, S, i, j, x ij {0,1} x ij i S, j S Les deux premières contraintes traduisent le fait que le degré sortant (resp. entrant) d un noeud dans un circuit vaut 1, la troisième contrainte interdit les solutions composées de sous-tours disjoints. Enfin, on impose aux variables x e de représenter des valeurs Booléennes. Remarquons enfin dans cette modélisation linéaire que si l'on omet la contrainte sur le degré sortant des noeuds ( i, j x ij = 1), le problème revient à trouver un arbre couvrant de poids minimal (ce problème est polynomial). On montrera ( 4.3.4) comment utiliser cette relaxation du problème pour obtenir des bornes inférieures de grande qualité. Si l'on veut rajouter des fenêtres de temps, la formalisation linéaire s'étend facilement en représentant la date de visite de chaque nœud i par une variable t i. Le problème devient alors : 2 2
70 min d ij x ij i, j i, x ij =1, j, x j ij =1 i S V, S, i, j, x ij {0,1} x ij i S, j S 2 t depot = 0, i, j, t j t i + x ij d ij i, a i t i b i 4.2 Algorithmes de résolution exacte Programmation linéaire en nombres entiers L'examen des techniques de résolution linéaire sur le problème du TSP est intéressant d'un point de vue historique, car les méthodes de programmation linéaire ont été bâties pour ce problème en particulier. La formulation linéaire présentée ci-dessus a deux inconvénients. 1. Le nombre de contraintes d élimination de sous-tours est exponentiel. Même la méthode de génération de colonnes évoquée au chapitre 1, qui permet d'éviter de stocker ces inéquations de manière explicite, serait pénalisée en temps par cette formulation exponentielle du problème. On va donc utiliser ces contraintes linéaires comme des plans de coupes : dès que l'algorithme de programmation linéaire (le simplexe, par exemple) renvoie une solution au problème simplifié (sans les contraintes d'élimination de sous-tours), on cherche une contrainte d'élimination de sous-tours violée par la solution, on l'ajoute au programme linéaire et on résout le nouveau problème. On peut ainsi réinjecter une à une, des contraintes d'élimination de sous-tours. Ceci peut être fait de manière très efficace car pour identifier une contrainte de sous-tours violée il suffit de résoudre un problème de coupe minimale. Ce problème est équivalent au problème du flot maximal, et peut donc être résolu en temps polynomial. = Une fois que l'on dispose d'une solution qui vérifie toutes les contraintes énoncées ci-dessus, celle-ci est généralement à valeurs fractionnaires. Et il est difficile de la transformer en une solution à valeurs entières. En effet, on ne connaît aujourd hui que des familles de plans de coupes qui séparent ces solutions; la description exacte du polytope des solutions entières est elle, encore inconnue (parmi les plans de coupes célèbres, on peut citer les inégalités du peigne proposées par Chvàtal [ABCC 94]). Ainsi, pour résoudre un problème de TSP en utilisant la modélisation linéaire, on est amené à développer un arbre de recherche en posant des points de choix sur les valeurs des variables qui sont fractionnaires dans la solution optimale courante. A chaque nœud de cet arbre, on rajoute des contraintes au programme linéaire courant. De multiples algorithmes peuvent être obtenus suivant le nombre de contraintes rajoutées à chaque itération. = Si l'on se contente de rajouter la contrainte d'affectation de la variable du point de choix, on obtient la version la plus simple d'algorithme de branch and bound. Pour le cas asymétrique (ATSP), les algorithmes de branch and bound sont généralement très efficaces, car la borne inférieure est très précise (proche de la valeur optimale). = On peut aussi obtenir des algorithmes qui font plus de raisonnement à chaque étape en rajoutant des contraintes d'élimination de sous-tours ainsi que des plans de coupes. Cette dernière famille d'algorithme qui conduit à l'exploration d'arbres de recherche plus petits, s'appelle le branch and cut, et a été développé pour le TSP par [PR 91]. Ces algorithmes sont particulièrement utilisés sur les 3
71 problèmes symétriques pour lesquels le branch and bound est peu efficace (à cause de la piètre qualité de la borne inférieure linéaire). Ces algorithmes permettent la résolution de problèmes de TSP jusqu'à un millier de villes et de TSPTW de l'ordre de quelques centaines à un millier de villes Programmation dynamique. La programmation dynamique est une technique générale de résolution exacte de problèmes d'optimisation qui consiste à énumérer les solutions du problème. Cependant, contrairement à la programmation par contraintes qui énumère de manière implicite l'ensemble des solutions, et évite de les générer toutes en rejetant des ensembles de solutions pour lesquels on prouve que la valeur de la fonction objective ne sera pas assez intéressante, la programmation dynamique, elle, procède à une énumération explicite de l'ensemble des solutions. Quand la programmation par contraintes s'efforce de propager de manière aussi efficace que possible les contraintes pour couper autant que possible l'arbre de rechercher et est ainsi parfois amenée à faire des calculs lourds à chaque nœud de l'arbre, la programmation dynamique prend le contre-pied de cette approche et cherche à factoriser autant que possible les calculs pour faire le minimum de travail possible à chaque étape et éviter tout raisonnement redondant. La programmation dynamique n'énumère ainsi pas toutes les solutions séparément les unes des autres, mais filtre l'ensemble des solutions pour énumérer des groupes de solutions. Il s'agit ainsi d'une énumération factorisée et complète. La programmation dynamique travaille sur une formulation récursive du problème, pour laquelle on fait de la tabulation des résultats intermédiaires. Prenons par exemple, le calcul de la suite de Fibonacci. Plutôt que de recalculer fib(n-1) et fib(n-2) pour obtenir fib(n), ce qui conduit à une complexité exponentielle, le calcul par programmation dynamique commence du cas de base en stockant fib(0) et fib(1), utilise la formule de récurrence du bas vers le haut, mémoïse tous les résultats intermédiaires, jusqu'à obtenir le résultats désiré. Cette technique nécessite un stockage compact des résultats (pour des raisons évidentes de taille mémoire). Pour trouver une formulation par récurrence du TSP, on ouvre le cycle et on appelle chaîne Hamiltonienne tout chemin passant une et une seule fois par chaque sommet. Soit alors V={0,...,n} et soit 0 le dépôt, pour x {1,.., n} et S {1,.., n}, x S, on note f(s,x) la longueur de la plus petite chaîne Hamiltonienne partant du dépôt, visitant tous les sommets de S et terminant en x. f peut être calculée par une fonction de récurrence : f (S, x) = min y S f (S {y}, y) + d(y, x) ( ) et la valeur du tour optimal est f({1,.., n}, 0). Le calcul du tour optimal nécessite de stocker n2 n valeurs de f (on est en effet amené à considérer les 2 n parties de {1,.., n} pour le premier argument et toutes les valeurs de {1,.., n} pour le second). L'utilisation astucieuse de la formule de récurrence permet a ainsi permis de diminuer la complexité par rapport à une énumération naïve des n! solutions possibles). Si l on stocke ces valeurs dans un tableau d entiers, en utilisant un codage binaire (par vecteur de bits) de S, alors, on peut remplir le tableau en le parcourant par index croissants (le codage étant croissant pour l inclusion d ensembles, quand on veut remplir f(s,x), les valeurs de f pour tous les sous ensembles de S ont déjà été calculées et on peut appliquer la formule de récurrence). Cet algorithme peut se généraliser aisément au cas du TSPTW. Par exemple, si l'on minimise le temps total de parcours, on dénote par f(s,x) le temps minimal pour partir du dépôt, visiter tous les sommets de S et terminer en x. On peut alors modifier la formule de récurrence comme suit : 4
72 f (, x) = max(a x,d(0, x)) si d(0,x) b x + sinon f (S, x) = min y S gx, ( y,s {y} ) g(, x) = max( f (y,t) + d(y,x), a x ) si f (y,t) + d(y, x) b x + sinon L intérêt de la résolution du TSP par programmation dynamique réside d'une part dans la rapidité des calculs et d'autres part dans la possibilité d'intégrer des contraintes supplémentaires (on a donné l'exemple des fenêtres de temps, on aurait aussi bien évoquer les contraintes de capacité ou d'autres encore); l inconvénient est la taille de la mémoïsation des calculs. On peut ainsi résoudre des problèmes de 10 noeuds en 100 ms. et de 15 noeuds en 30 s. Au delà, la mémoire requise pour le tableau devient rapidement déraisonnable. Cette méthode n'est donc applicable que pour de très petits problèmes. Mentionnons qu'une autre approche utilisant la programmation dynamique est capable de résoudre des problèmes de taille plus importante [Du 95]. Mais cet algorithme est beaucoup plus complexe et repose sur une discrétisation très forte des données (distances, fenêtre de temps) qui peut parfois changer suffisamment le problème au point rendre certaines solutions irréalisables ou de rendre réalisables certaines solution qui violent des contraintes. 4.3 Contraintes On s'intéresse à la résolution du TSP en programmation par contraintes. La première constatation que l'on peut faire est que les contraintes arithmétiques classiques sur les domaines finis (+,, =,, min/max, etc.) ne permettent pas d'exprimer simplement la configuration de cycle Hamiltonien. Les systèmes usuels ont donc été amenés à introduire des contraintes globales spécialisées pour ces situations : on a ainsi cycle dans CHIP [BC 94] et IlcPath dans ILOG SOLVER. Nous allons proposer une technique d'implémentation de ces contraintes globales Modélisation Alors que dans la formulation linéaire, une solution au TSP est un ensemble d arêtes vérifiant toute une série de contraintes additionnelles, la programmation par contraintes, plus expressive, permet d incorporer une partie de ces contraintes (par exemple celles sur le degré sortant des noeuds) dans la formulation même du problème. Ainsi, on orientera le tour et on modélisera une solution au TSP par une relation Next : V V associant à chaque sommet son successeur immédiat. Next doit ainsi vérifier certaines contraintes additionnelles : = Next est bijective. Si l on duplique l ensemble des sommets pour considérer la relation Next dans un graphe biparti, cette contrainte affirme que Next est un couplage dans un graphe biparti. La propagation simple de cette contrainte consiste, après chaque affectation Next(x):=y à retirer y de tous les domaines de Next(z) pour z x. = Next ne contient pas de cycle de longueur inférieure à n : Ainsi, quand une chaîne x 1,...,x k a été construite pour Next, on retire x 1 du domaine de Next(x k ). Un algorithme de propagation de cette contrainte d'élimination de sous-cycles est proposé dans [CL 97b]. Dans le cas du TSPTW, on peut envisager d'autres modélisations inspirées du problème d'ordonnancement disjonctif. = On peut modéliser le problème comme un problème d'ordonnancement de tâches dont les durées représentent le temps de trajet depuis la tâche précédente. Les durées ne sont donc plus des données, 5
73 mais dépendent de la solution. Pour appliquer des règles de coupe dérivées de celles des intervalles de tâches, on est amené à estimer ces durées, c'est à dire évaluer des bornes inférieures des temps de trajets entre un nœud et ses prédecesseurs potentiels. Ces règles de propagation peuvent ensuite être utilisées à l'intérieur d'un arbre de recherche binaire raisonnant sur l'ordre respectif de deux tâches dans le tour. = Une troisième solution consiste à reprendre le modèle de séquence [Zh 97] où l'on choisit d'affecter à une tâche une position dans le tour. Pour ce modèle, on peut décrire des règles similaires à celles du modèle d'ordonnancement (avec la même difficulté liée à l'estimation de la durée d'une tâche). Le modèle le plus adapté dépend du type de données. Quand le problème est dominé par les contraintes temporelles, il vaut mieux utiliser le schéma de branchement inspiré du job-shop. Dans certains cas, où ni l'aspect temporel ni l'aspect géographique ne domine, le modèle par séquence peut être le plus intéressant. Dans les autres cas du TSP pur, ou d'un TSPTW avec quelques contraintes temporelles (ce qui semble être le cas le plus réaliste), le modèle issu des couplages est le plus adapté au problème. Parmi les techniques de résolution des problèmes de couplages bipartis (décrites au chapitre 2), on retient l'utilité de maintenir une relation redondante symétrique Prev : V V (Celle-ci est en particulier indispensable pour le cas où les distances sont asymétriques) et l'évaluation d'une borne inférieure de la longueur du tour, LB, en sommant les distances de chaque nœud à son plus proche voisin. Cette borne inférieure permet d éliminer certaines arêtes du graphe (les arêtes ij telles que d ij - best(i) UB - LB). On gardera aussi le terme correctif sur cette borne permettant de prendre en compte à l'avance les regrets qui feront augmenter la borne. Enfin, on sélectionnera les points de choix par un critère mixte qui considère le regret pour les domaines de cardinalité importante et le first-fail pour ceux de cardinalité faible. L ensemble de ces techniques directement issues de la modélisation du TSP comme un problème de couplage ne permettent de résoudre que des problèmes jusqu à 12 ou 15 noeuds. La contrainte d élimination des sous-tours, qui fait la particularité du TSP par rapport au problème des couplage est en effet trop peu propagée Propagation en présence de fenêtres de temps Comme pour l'ordonnancement disjonctif, on peut réduire dynamiquement les fenêtres de temps attachées à chaque nœud. Pour cela, on a enrichit la modélisation du problème de deux relations redondantes. On associe à chaque tâche t une fenêtre de temps (dynamique) [t,t] initialisée à la fenêtre imposée [a t, b t ] et que l'on va réduire au cours de la résolution. On associe aussi à chaque tâche t une liste d'autres tâches before(t) (resp. after(t)) représentant l'ensemble des tâches effectuées avant (resp. après) t. Ainsi, la relation after (resp. before) représente la fermeture transitive de la relation next (resp. prev). L'ensemble de ces structures internes peut être utilisé pour mettre en œuvre les règles de propagation suivantes : = règles structurelles de la fermeture transitive y = next(x) y after(x) z after(y) y after(x) y after(x) next(y) x = propagation des précédences sur les fenêtres de temps (PERT) : y after(x) = propagation des fenêtres de temps sur les précédences next(x) z y x +d(x,y) x y d(x,y) ( ) y min x domain(prev(y)) x + d(x, y) 6
74 = "edge finding" limité à deux tâches : y + d(y, x) > x y after(x) Des règles similaires impliquent les relations symétriques (prev, before). L'ensemble de ces règles de propagation relativement simples peuvent être enrichies d'autres règles de propagation globales, plus complexes, que nous décrivons dans les paragraphes qui suivent Contraintes redondantes: connectivité, isthmes On propose maintenant une contrainte redondante pour mieux propager la contrainte structurelle de circuit Hamiltonien. En effet, pour l'instant, la seule contrainte structurelle que l'on rajoute à la modélisation comme couplage biparti est la contrainte d'élimination de sous-tours qui interdit une arête à chaque affectation (l'arête qui fermerait la chaîne dans laquelle on vient d'insérer une nouvelle arête en un sous tour). On peut renforcer cette propagation de manière significative si l'on remarque que la contrainte d'interdiction de sous-tours est une contraintes de connexité : en effet, un circuit Hamiltonien est un ensemble connexe d'arêtes, alors qu'un ensemble Hamiltonien de sous-circuits a autant de composantes connexes que de tours. On peut ainsi vérifier à chaque noeud de la recherche que le graphe (dynamique) des arêtes autorisées (formé par les domaines de la relation next) est connexe. Attention, on n'impose pas que l'ensemble des arêtes sélectionnées jusqu'à cette étape du calcul soit connexe, mais que l'ensemble des arêtes encore sélectionnables soit connexe. Ce test permet de rejeter de nombreuses situations dans lesquelles il n'existe plus aucun circuit Hamiltonien. On peut aussi faire une vérification plus fine: puisqu on dispose par la modélisation en couplage biparti des relations prev et next qui portent sur des arcs orientés, on peut vérifier une condition beaucoup plus forte que la simple connexité: la forte connexité, qui assure qu'entre deux sommets il existe toujours un chemin faits d'arcs orientés, de l'un vers l'autre et vice-versa. La vérification de la forte connexité se fait ainsi par un parcours en largeur du graphe (algorithme en O(m) ). On peut proposer une propagation encore plus forte de la contrainte de connexité en cherchant à détecter à l'avance les arcs (appelés isthmes) qui sont indispensables à la forte connexité du graphe. Ainsi quand un arc e est tel que si on le supprime du graphe, celui n'est plus connexe, e peut être immédiatement sélectionné dans le tour. La détection d'isthmes ne peut pas se faire, à notre connaissance, de manière aussi simple que la vérification de la (forte) connexité, et a une complexité en O(mn). Si la simple vérification de la (forte) connexité permet d accélérer la résolution, la détection d'isthmes ne permet de gagner qu'un seul retour arrière par isthme (puisque sans cette propagation, si l'on ne sélectionnait pas un isthme dans le tour, on s'apercevrait au noeud suivant de l'arbre de recherche que le graphe ne serait plus fortement connexe, et on abandonnerait la branche). Le surcoût en complexité d'un facteur n n'est expérimentalement pas justifié par la réduction de l'espace de recherche qu'il procure Bornes inférieures Relaxation en couplage parfait de poids minimum La modélisation du TSP comme un problème de couplage biparti permet d'obtenir facilement des bornes inférieures sur la longueur minimale du tour. Comme on l'a mentionné plus haut, on peut obtenir simplement une borne inférieure grossière en sommant les distances des nœuds à leurs plus proches voisins. Cette borne peut être raffinée en y ajoutant un terme correctif prenant en compte à l'avance l'ajout de regrets. 7
75 Une première idée pour renforcer encore cette borne serait de calculer de manière exacte la valeur du couplage optimal dans le graphe. Ceci peut être fait avec une complexité faible en utilisant la méthode Hongroise incrémentale [CL 97b]. Ces trois bornes associées à la relaxation du TSP en un problème de couplage parfait de poids minimal fournissent des résultats irréguliers suivant la distribution des données. Dans le cas d'une distance symétrique, le couplage optimal a tendance à contenir de nombreux cycles élémentaires à deux éléments : si x est le plus proche voisin de y, y a de bonnes chances d'être le plus proche voisin de x, et donc le couplage optimal a de fortes chances de contenir le cycle xyx, ce qui l'éloigne d'un cycle Hamiltonien, et donc la borne inférieure peut être de piètre qualité [BT 86]. Une autre situation dans laquelle la relaxation du couplage peut fournir une borne imprécise se produit lorsque les villes sont réparties géographiquement par agrégats (clusters). Dans ce cas, chaque ville a une voisin très proche, et le couplage optimal ne tient compte que de ces distances locales, en oubliant que les distances (plus importantes) entre agrégats devront aussi être parcourues puisqu on ne veut pas un tour par agrégat, mais un seul et unique tour. Pour toutes ces raisons, on cherche à disposer d'une autre borne inférieure, plus robuste par rapport aux distributions de données. C'est l'objet du paragraphe suivant qui explore les relaxations en problèmes d'arbre couvrant de poids minimum Relaxation en arbre couvrant de poids minimum Si l on oublie, dans la formulation linéaire du TSP, les contraintes sur les degrés sortant des sommets, le problème se relaxe en un problème d arbre couvrant de poids minimal (MST pour Minimal Spanning Tree). En effet, pour peu que l'on duplique un sommet (disons que 1 est dupliqué en {1,n+1}), les cycles Hamiltoniens correspondent aux chaînes Hamiltoniennes entre 1 et n+1. Or une chaîne Hamiltonienne est un arbre couvrant particulier dont tous les noeuds ont un degré sortant égal à 1. Le problème MST est donc une relaxation du TSP. Or, trouver un arbre couvrant de poids minimal est un problème polynomial, que le graphe soit orienté [CLR 86] ou non. cas non orienté : algorithme de Prim Trouver un MST dans un graphe non orienté est extrêmement facile: deux algorithmes gloutons résolvent le problème de manière naturelle. L algorithme de Prim ([Pr 57], cf. [CLR 86]) part d un arbre réduit à un sommet et lui rajoute à chaque étape une arête incidente de poids minimal (cette arête relie donc un sommet de l arbre à un sommet extérieur). L arbre obtenu après n-1 insertions est un MST. La complexité est en O(n 3 ) ou O(n 2 ) pour une implémentation soignée. L autre algorithme glouton classique est celui de Kruskal ([Kr 56], cf. [CLR 86]) qui part d un ensemble d arêtes vide et ajoute une par une les arêtes de poids minimal (non nécessairement incidentes) qui ne créent pas de cycle avec l ensemble déjà sélectionné. La complexité est en O(m 2 ) ou O(m.log(n)) pour une implémentation soignée. En pratique, l algorithme de Prim est plus simple et plus rapide sur les graphes de taille petite ou moyenne. cas orienté : algorithme d Edmonds Dans le cas d'un graphe orienté, on ne s'intéresse plus aux arbres couvrants, mais aux arborescences couvrantes. Soit s un noeud (qu'on prend comme racine), une arborescence est un ensemble de n-1 arcs de sorte que de tout nœud sauf s ne parte qu'un seul arc et tel qu'il n'y ait aucun cycle. On peut voir une arborescence comme un arbre où tous les arcs sont orientés vers la racine s. Une chaîne Hamiltonienne est non seulement un arbre couvrant, c'est aussi une arborescence couvrante. Ceci nous donne donc une deuxième relaxation plus précise que la précédente : celle de l'arborescence couvrante de poids minimal. Ce problème peut être résolu par l'algorithme d Edmonds [Edm 71] qui 8
76 construit, pour un sommet s fixé, une arborescence de racine s de poids minimal (notée s-msa). Le principe de construction ressemble à un algorithme glouton: on fixe le sommet racine s, et on part d un ensemble d arêtes F=. On considère successivement les n-1 sommets y s et l on ajoute à F l arc xy de poids minimal. On obtient un graphe orienté (V,F) couvrant tous les sommets, mais qui n est pas forcément une arborescence (auquel cas, il n est pas connexe non plus). En effet, il se peut que l addition de sommets ait créé des cycles. On va transformer ce graphe petit à petit en une arborescence en ouvrant un à un les cycles. Plus précisément, soit µ un cycle dans (V,F), on construit à partir de G=(V,F) un graphe contracté G =G/µ=(V,F ) dans lequel on a remplacé le cycle µ par un seul sommet. On étend la distance de G à G' en posant : = d (x,y) = d(x,y) si xy n est pas un arc entrant dans µ= = d (x,y) = d(x,y) - d(z,y) où z est le prédécesseur de y dans µ sinon. (on fait cette modification pour avoir d(f) = d (F ) + d(µ)). L ensemble d arêtes F dans le graphe contracté contient ainsi un cycle de moins que F. Après une série de telles transformations, on obtient une s-msa F (k) pour un graphe contracté G (k) et cette arborecence est de poids minimal. On revient ensuite progressivement au graphe de départ G par une suite de s-msa dans les graphes G (k) en expansant progressivement les cycles que l on avait contracté : On commence avec l arborescence A (k) = F (k). Supposons ensuite qu entre G (k-1) et G (k), on ait contracté un cycle µ en un noeud α, soit αβ l arc partant de α sélectionné dans A (k), cet arc correspond à yβ dans le graphe F (k-1). On obtient alors A (k-1) à partir de A (k) en expansant le noeud α ainsi: on garde tous les arcs de µ sauf xy. A (k-1) est alors une s-msa dans le graphe G (k-1). On expanse ainsi tous les cycles, jusqu à obtenir une arborescence couvrant A (0) dont on peut prouver qu elle est de poids minimal: d(a (0) ) = d (k) (A (k) ) + d (i) (µ i ) i où µ i est le cycle qui a été contracté entre G (i) et G (i+1). On peut donc se passer des phases d expansion si on ne cherche que la valeur de la s-msa. La complexité de cet algorithme est en O(mn). On pourrait calculer la valeur de la s-msa pour tous les sommets racine s. En fait, on préfère procéder à un calcul pour le graphe des domaines de la relation next et un calcul pour le graphe des domaines de la relation prev, ce qui correspond aux deux orientations possibles de l'arborescence. On va voir de manière expérimentale que cette borne permet de diminuer grandement la taille des arbres de recherche développés, mais que son coût en temps annule le gain en taille de l'arbre de recherche. L'application de relaxation Lagrangienne au calcul de s-msa va permettre d'affiner grandement la borne au prix d'un surcoût faible en temps de calcul et va ainsi améliorer substantiellement les performances de l'algorithme de résolution. Relaxation Lagrangienne, borne inférieure de Held & Karp Comme on l'a décrit plus haut, le problème de la s-msa est une relaxation polynomiale du TSP. Held et Karp ont proposé [HK 71] d'affiner cette relaxation en lui appliquant le principe de la relaxation Lagrangienne : on fait passer dans la fonction objective les contraintes qui imposent que les degrés sortants valent 1. Les multiplicateurs de Lagrange (cf ) sont ainsi des poids π i attachés à chaque sommet. Ces poids peuvent être vus comme des pénalités puisque la fonction objective est transformée en : (d(ij) +π i ) ij MST = d(ij) + π i δ i ij MST i (on prend π i = 0 ) 9
77 où δ i est le degré sortant de i moins 1. Le vecteur δ des excès de degré sortant forme un sous-gradient. On va ensuite faire évoluer les multiplicateurs de Lagrange en donnant des pénalités positives aux sommets de fort degré sortant dans le MST et en donnant des pénalités négatives aux feuilles du MST. Ces pénalités visent à rendre le MST plus filiforme, et on remarquera ici l analogie avec le calcul du terme correctif de la borne supérieure du couplage de poids minimal par prise en compte à l'avance de regrets. Pour la mise à jour des multiplicateurs, on suit la méthode proposée par Beasley [Bea 93] : on pose π i := π i + α(ub LB) δ i δ i 2 où α est un facteur de l ordre de l unité qui décroît avec le temps, UB est une borne supérieure et LB une borne inférieure. Le principe de cette marche ressemble à du recuit simulé: on avance à grands pas tant qu on est loin de l optimum, puis à mesure qu on s en rapproche, on diminue ses pas. LB est réactualisé à chaque nouvelle solution trouvée pour le problème paramétrique de MST. Ces paramètres demandent à être réglés de manière fine pour que les multiplicateurs convergent rapidement non loin de l optimum (en particulier, si l'on veut utiliser cette borne inférieure à chaque nœud d'un arbre de recherche, il faut converger vers une bonne solution en peu d'itérations) Recherche arborescente Nous avions mentionné pour la résolution du problème du couplage biparti de poids minimal deux constructions possibles de l'arbre de recherche : pour un sommet X sélectionné, on pouvait soit essayer toutes les arêtes par ordre de poids décroissant, soit poser l'alternative binaire X est associé à son meilleur choix ou X ne l'est pas, sans préciser plus la seconde branche. Cette deuxième construction se justifie par le fait que la décision X n'est pas associé à son meilleur candidat suffit à faire augmenter la borne inférieure naïve du couplage et évite d examiner plusieurs branches similaires quand tous les seconds choix sont relativement indifférents pour X. Alors que ce schéma de branchement avait eu un comportement parfois instable pour le problème du couplage (programme PPC3, tableau 2-1), il s'avère nettement plus efficace que le premier dans le TSP. Pour l optimisation on préfère redémarrer un arbre nouveau à chaque solution trouvée plutôt que d explorer un seul arbre de manière exhaustive en diminuant dynamiquement la valeur de la borne supérieure à chaque solution trouvée. Ceci s explique par l'explosion combinatoire lores de la recherche de solutions proches de l'optimum : dès qu on a trouvé une solution, le problème suivant (en trouver une meilleure) est sensiblement plus difficile. Quitte à réexplorer une partie de l arbre, on préfère recommencer pour explorer un arbre dont la structure soit la mieux adaptée possible au problème (dont les noeuds près de la racine réduisent la complexité de la recherche) Résultats expérimentaux Nous avons maintenant décrits tous les composants d'une résolution en programmation par contraintes : les modèles, les algorithmes de propagation et leurs structures internes associées, les contraintes redondantes et une série de bornes inférieures, des stratégies de branchement et un schéma d'optimisation. Nous pouvons maintenant décrire rapidement quelques expérimentations permettant de valider ce cadre algorithmique. Toutes les expériences on été faites sur des problèmes à distance symétrique pour éviter de surévaluer la qualité de l'algorithme en l'évaluant sur des problèmes asymériques pour lesquels la borne inférieure issue du modèle de couplage biparti est très précise. La méthode présentée est générique et pourrait éventuellement donner de meilleurs résultats sur des problèmes asymétriques. Le tableau 4-1 compare différents algorithmes de programmation par contraintes sur des problèmes entre 10 et 21 nœuds (nous avons généré aléatoirement les problèmes cl à 10, 13, 15 et 20 nœuds, et les problèmes gr17 et gr21 viennent de la collection TSPLIB [Rei 94]). Le premier programme propage de 10
78 manière naïve la contrainte de couplage, le second y ajoute la modélisation symétrique et les bornes symétriques, l'utilisation du regret, le troisième ajoute aux bornes inférieures le terme correctif lié au regret, et vérifie la contrainte redondante de connexité forte. Le quatrième parcourt un arbre de décision binaire. Enfin, le dernier intègre tous ces composants et y ajoute le calcul d'une arborescence de poids minimal comme borne inférieure. cl 10 cl 13 cl 15 gr 17 cl 20 gr 21 PPC naïve 2.3 kb. 0.3 s. 9.2 kb. 2 s. 120 kb. 16 s Mb. 3.5 ks. 7.6 Mb. 1.1 ks Mb. 3 ks. PPC "couplage symétrique" 1.8 kb. 0.3 s. 8.7 kb. 1.5 s kb. 8 s. 315 kb. 85 s. 676 kb. 165 s. 180 kb. 44 s. Propagation complète 146 b. 0.1 s. 717 b. 0.5 s. 2.4 kb. 1.9 s kb. 12 s. 65 kb. 67 s kb. 14 s. Propagation complète et arbre binaire 118 b s. 583 b. 0.2 s. 2 kb. 0.9 s. 5.8 kb. 3.1 s. 27 kb. 15 s kb. 7 s. Idem + s-msa 112 b s. 553 b. 0.3 s. 1.7 kb. 1 s. 5.5 kb. 3.5 s kb. 18 s kb. 9 s. Tableau 4-1: différents algorithmes de PPC pour la résolution de petits TSP (10-21 nœuds) Le meilleur de ces algorithmes (celui faisant une propagation complète et parcourant un arbre binaire) peut résoudre des problèmes jusqu'à une trentaine de nœuds. On donne à titre d'exemple dans le tableau 4-2 la résolution de huit problèmes entre 20 et 29 nœuds, avec ou sans fenêtres de temps (les 4 premiers proviennent de TSPLIB, les suivants sont issus d'un problème de tournées [So 87],[PGPR 96]). On remarquera que les fenêtres de temps, en contraignant plus le problème, facilitent sa résolution (le phénomère inverse se produit pour les approches linéaires). gr 24 TSP (n=24) fri 26 TSP (n=24) bayg 29 TSP (n=29) bays 29 TSP (n=29) rc201.0 TSPTW (n=26) rc201.1 TSPTW (n=29) rc201.2 TSPTW (n=29) rc201.3 TSPTW (n=20) PPC 6.6 kb. 934 kb Mb. 1.1 Mb. 542 b. 649 b b. 158 b. 6.9 s. 930 s. 4.4 ks. 1.2 ks. 5.8 s. 8.2 s. 19 s. 1.8 s. Tableau 4-2: résolution de problèmes TSP(TW) entre 20 et 29 nœuds à l'aide de P.P.C. Enfin, le tableau 4-3 illustre l'efficacité de l'utilisation de la relaxation Lagrangienne comme technique de borne inférieure (les problèmes résolus sont encore issus de TSPLIB [Rei 94]). Dans ce cas, on peut avoir intérêt à changer de schéma de branchement pour sélectionner en priorité les sommets dont le degré sortant est le plus fort dans la MSA. Choisir l'arête incidente concernant ce sommet permet non seulement d'améliorer la qualité de la borne inférieure au calcul suivant de MSA, mais cette stratégie conduit aussi à obtenir des solutions (des MSA qui sont en fait des chaînes Hamiltoniennes) beaucoup plus tôt dans l'arbre. Ceci permet de limiter la profondeur de l'arbre de recherche parcouru. 11
79 PPC avec une borne inf. de relaxation Lagrangienne bayg29 dantzig42 att48 hk48 brazil58 st70 4 b. 46 b. 60 b. 127 b. 42 b. 172 b. 14 s. 437 s. 180 s. 330 s. 1.1 ks. 3.3 ks. Tableau 4-3: utilisation de la relaxation Lagrangienne sur le calcul d'une MSA comme borne inférieure pour la résolution de TSP de 29 à 70 nœuds 4.4 Solutions heuristiques. Après avoir décrit trois familles de méthodes pour résoudre de manière exacte le TSP (méthodes linéaires, programmation dynamique et programmation par contraintes), on s'intéresse maintenant aux techniques permettant de fournir une réponse approchée au problème d'optimisation. On abordera deux classes d'algorithmes : les méthodes heuristiques pour construire une solution et les méthodes d'optimisation locale pour améliorer une solution Heuristiques gloutonnes de construction de solution Il existe de nombreuses procédures de construction heuristiques d'une solution au TSP. Parmi les plus courantes, on peut citer : 1. L'heuristique du plus proche voisin. Cet algorithme glouton construit le cycle en faisant croître une chaîne. On part d'un sommet arbitraire à partir duquel on va au sommet voisin le plus proche, puis de celui-là à son plus proche voisin non visité, etc..., jusqu à ce que tous les sommets aient été parcourus, où l on revient au départ. Cet algorithme (valable pour les graphes complets) est en O(n 2 ), et on a prouvé que les solutions qu il fournit peuvent être arbitrairement mauvaises (voir [Rei 94]). En effet, cette procédure commence généralement par faire de très bons choix en sélectionnant des arêtes de poids faible, mais, vers la fin, la chaîne doit ensuite aller visiter des sommets qui ont été oubliés, et des distances importantes sont alors rajoutées à la chaîne. 2. Les algorithmes d'insertion. Ces algorithmes fonctionnent de la manière suivante : on part d un tour réduit à quelques sommets (par exemple, un boucle entre 2 sommets), puis on sélectionne un sommet hors du tour que l on insère entre deux sommets du tour (à la place qui fait augmenter le moins possible la longueur totale de celui-ci). On insère ainsi tous les sommets jusqu à obtenir un circuit de longueur n. Le choix du sommet à insérer peut être fait suivant de nombreux critères : = plus proche (resp. plus lointaine) insertion: on peut insérer le sommet dont la distance minimale à un sommet du tour est minimale (resp. maximale), = insertion au hasard: on peut insérer un sommet choisi au hasard, = insertion de coût minimal (resp. maximal): on peut insérer le sommet qui fera le moins (resp. le plus) augmenter la longueur du tour partiel. 3. L'algorithme des économies. Un troisième solution, l algorithme des savings de Clarke et Wright [CW 64] est issue de la résolution du problème des tournées: on particularise un sommet v 1 et l on crée n-1 tours élémentaires: des boucles v 1 -v i -v 1. On procède ensuite à n-2 étapes de fusion où l on prend deux tours v 1 -v σ(1) -...-v σ(i) -v 1 et v 1 -v ξ(1) -...-v ξ(k) -v 1 que l on fusionne en v 1 -v σ(1) -...-v σ(i) -v ξ(1) -...-v ξ(k) -v 1. Une telle fusion permet d'éviter un aller-retour au sommet v 1 et diminue la somme des longueur des tours de la quantité d(σ(i),1) + d(1,ξ(k)) - d(σ(i),ξ(k)). L algorithme consiste à effectuer à chaque fois la fusion qui permettra de diminuer le plus possible la somme de ces longueurs. 12
80 4. Sélection gloutonne d'arêtes. Il s agit d un algorithme glouton classique, où l on sélectionne les arêtes une à une, sans nécessairement avoir une chaîne à tout moment. On commence donc par l arête de poids le plus faible, puis l on sélectionne l arête de poids minimal qui ne crée pas de sous-cycle, et ainsi de suite. Cette procédure devient un peu plus complexe dans le cas de graphes non complets: on doit alors faire un peu de backtracking. Le tableau 4-4, obtenu à partir des résultats de [Rei 94], présente une comparaison des ces différentes procédures heuristiques sur de très grands problèmes (les chiffres sont donc un peu pessimistes pour les tailles de problèmes qui nous intéressent). HEURISTIQUE distance moyenne à l optimum savings 11% greedy 12% plus lointaine insertion 23% insertion au hasard 23,5% plus proche insertion 29,5% plus proche voisin 26% insertion de coût minimal 34% Tableau 4-4: comparaison d'heuristiques pour le TSP [Rei 94] Il existe encore bien d autres heuristiques basées sur des méthodes géométriques spécifiques à la distance considérée et utilisant par exemple l enveloppe convexe des points, ou une triangulation de l'espace ou encore utilisant l arbre couvrant de poids minimal, etc. Dans le cadre du TSPTW, l'application de ces procédures devient beaucoup plus problématique. Le problème de la satisfiabilité (trouver un circuit admissible) peut devenir difficile, et ces méthodes ne sont plus toujours à même de produire une solution (elles peuvent s'arrêter en échec si elles n'arrivent plus, par exemple, à insérer une tâche pour des raisons de contraintes temporelles). D'autre part, les bonnes solutions du TSPTW peuvent être éloignées des bonnes solutions au problème de TSP dans lequel on a relaxé les contraintes temporelles : il devient ainsi beaucoup plus difficile de trouver des critères pertinents pour guider la construction incrémentale du tour. On a en effet deux objectifs qui peuvent être contradictoires : d'une part, produire un tour admissible, c'est à dire une séquence des villes compatible avec les fenêtres de temps et d'autre part, produire un tour de faible longueur totale, c'est à dire une séquence basée sur les relations de proximités géographique. On est donc amené à privilégier les insertions qui non seulement n augmentent pas trop la longueur du cycle partiel, et aussi qui n'engendrent trop de temps d attente. Il est ainsi parfois difficile de juger de la pertinence d'une insertion dans un tour partiel au regard de ces deux critères parfois antagonistes (on verra au chapitre 7, comment la recherche LDS permet de résoudre de tels conflits). 4.5 Optimisation locale Une fois la solution de départ obtenue, celle-ci peut généralement être facilement améliorée par une série de transformations locales. 13
81 4.5.1 Déplacements Les transformation les plus simples qui permettent de transformer un tour en un autre tour sont basées sur les procédures gloutonnes précédentes, en utilisant le mécanisme d'insertion en avant et en arrière. A partir d'un tour donné, on peut obtenir une famille d'autres tours proches en supprimant un sommet (ou une arête) et en cherchant à le réinsérer ailleurs dans le tour. Ces transformations sont appelées le déplacement de sommet (ou d'arête). On peut ainsi chercher par une passe en O(n 2 ) s'il existe un déplacement de sommet qui diminue la longueur du tour et par une passe en O(mn) s'il existe un déplacement d'arête améliorant le tour. Figure 4-1: deux transformations locales pour le TSP, le déplacement de sommet et le déplacement d'arête Procédures 2-opt, 3-opt Une autre transformation possible consiste à effacer k arêtes du tour, ce qui a pour effet de couper celui-ci en k chaînes (séquences de nœuds) disjointes et à recomposer un autre tour en reconnectant ces chaînes d'une autre manière. L algorithme k-opt, qui essaye toutes les possibilités, a une complexité en O(n k ). Ces transformations nécessitent parfois de changer le sens de parcours de certaines chaînes. En particulier, 2- opt parcours toujours une des deux chaînes à rebours et 3-opt comporte 4 possibilités de transformations dont une seule ne rebrousse aucune chaîne. Changer l'orientation d'une chaîne ne pose pas de problèmes dans le cas du TSP symétrique pur, mais, dans le cas de TSP asymétriques, la longueur de la sous-chaîne dépend du sens de parcours, et en présence de fenêtres de temps (TSPTW) ou de contraintes de précédence, les deux sens de parcours ne sont pas forcément admissibles. Ainsi, ces procédures basées sur l'échange d'arêtes deviennent plus coûteuses et moins efficaces (car le voisinage en question contient moins de solutions admissibles) pour le TSPTW. Les voisinages définis par k-opt peuvent être explorés de nombreuses manières: la méthode classique (algorithme k-opt stricto sensu) applique une stratégie de descente qui consiste à aller d une solution à sa meilleure voisine, et améliorer ainsi de suite la solution jusqu à ce que la solution soit de coût minimal pour son voisinage. On peut cependant aussi utiliser ces voisinages pour faire des marches aléatoires, de la recherche tabou, etc. Les grandes valeurs de k conduisent ainsi aux meilleures améliorations, mais la complexité de l algorithme croit rapidement avec k. Dans la pratique, 3-opt fournit d excellents résultats et a l avantage de comporter une transformation qui ne retourne aucune chaîne (ce qui est important pour la robustesse aux contraintes supplémentaires). Quand 3-opt est trop lent pour la taille du problème à résoudre, on utilise 2-opt, ou une version réduite de 3-opt qui examine O(n 2 ) voisins de la solution (on réduit la complexité en limitant la taille d une des 3 chaînes). Cette procédure est souvent appelée 2 1 / 2 -opt ou OR-opt [Or 76]). 14
82 Figure 4-2: la transformation de la procédure 3-opt conservant l'orientation des sous-chaînes On peut définir les voisinages k-opt pour k arbitrairement grand. En pratique, on ne fait de l'exploration systématique de voisinage que pour n 3. Le paragraphe suivant décrit une méthode pour faire une exploration partielle de voisinages correspondant à des valeurs supérieures de k Procédure de Lin et Kernighan L algorithme de Lin et Kernighan [LK 73] est basé sur les voisinages de k-opt, avec deux particularités : d une part, ces voisinages sont (partiellement) explorés pour de grandes valeurs de k (on envisage donc des mouvements complexes), d'autre part, l'exploration est suffisamment limitée pour garder une complexité pratique inférieure à celle de 3-opt (en O(n 3 )). L idée de l algorithme est la suivante: une transformation locale consiste toujours à remplacer un ensemble d arêtes {e 1,..., e k } par {f 1,..., f k } : sans perdre trop de généralité, on peut considérer que ces deux ensembles sont classés de sorte que pour tout i, e i et f i se terminent sur le même sommet. On peut donc construire séquentiellement la transformation en remplaçant e 1 par f 1, e 2 et f 2, etc. Puisque la transformation améliore le tour, k (d ei d fi ) > 0. i=1 Le point-clé est de remarquer qu on peut alors ordonner les i de sorte que pour toutes les sommes partielles S l soient strictement positives l S l = (d ei d fi ) i =1 L algorithme consiste alors à partir d un sommet x dans le tour, à remplacer l arête e 1 =xx qui arrive sur x par une autre f 1 =yx plus courte (d(f 1 ) < d(e 1 )), puis à remplacer l arête e 2 =yy qui part de y dans le tour par une autre f 2 =zy telle que d(f 1 ) + d(f 2 ) < d(e 1 ) + d(e 2 ) et ainsi de suite. On intègre généralement à cette recherche un peu de backtracking pour les deux premiers niveaux de choix (y et z). On peut aussi vouloir contrôler la complexité de cette recherche en limitant le nombre total k d arêtes remises en jeu (par exemple, si l on ne veut que du 2-opt, ceci revient à imposer que x=z). Le tableau 4-5 résume une comparaison faite dans [Rei 94] de plusieurs procédure d optimisation locale sur une série de très grands problèmes. La méthode de Lin et Kernighan est de très loin la meilleure et fournit des solutions de qualité remarquable. 15
83 méthode d optimisation locale distance moyenne à l optimum LK 1% LK (limité à 2-opt et DS) 2% 3-opt 4% 2-opt et DS 6% déplacement de sommet (DS) 8,5% 2-opt 9% déplacement d arêtes 10% Tableau 4-5 : comparaison de méthodes d'optimisation locale pour le TSP [Rei 94] Comme on l'a mentionné, les performances des procédures d'optimisation locale se dégradent en présence de fenêtres de temps. De manière générale, l'optimisation locale se combine mal avec les contraintes supplémentaires. D'une part, il devient nécessaire de tester la satisfiabilité des contraintes pour chaque mouvement, ce qui peut être long. D'autre part, comme de nombreuses solutions du voisinage deviennent irréalisables, l'algorithme est plus rapidement "piégé" dans des optima locaux. 4.6 Bilan. En résumé, le problème du voyageur de commerce est aujourd'hui un problème bien connu, pour lequel on dispose de bonnes procédure heuristiques, et d'excellentes techniques d'optimisation locale (Lin & Kernighan) et de bornes inférieures (Held & Karp). Il est donc relativement aisé de fournir rapidement un bon encadrement de l'optimum. Pour le cas de problèmes asymétriques, la relaxation en un problème d'affectation bijective (en relachant les contraintes d'élimination de sous-tours) fournit d'excellentes bornes inférieures qui peuvent être utilisées dans le cadre d'une optimisation en branch&bound sur des problèmes de grande taille. Le cas symétrique est beaucoup plus difficile. Pour les petits problèmes, jusqu'à nœuds, la programmation dynamique et la programmation par contraintes sont deux techniques simples à mettre en œuvre, robustes aux contraintes supplémentaires, et qui fournissent de bons résultats. Si l'on veut résoudre très rapidement de tels petits problèmes (ce peut être le cas comme nous le verrons au chapitre 7 pour l'optimisation individuelle des routes dans un grand problème de tournées), la programmation par contraintes est la méthode la plus rapide. Pour les problèmes jusqu'à 30 nœuds, une approche soignée par contraintes permet la résolution aisée de problèmes avec ou sans contraintes additionnelles. Pour les problèmes entre 30 et 70 nœuds, la programmation par contraintes peut s'appliquer si on lui adjoint un calcul de borne inférieure utilisant le relaxation Lagrangienne. Jusqu'à 100 nœuds, on peut encore utiliser la programmation par contraintes et la relaxation Lagrangienne dans un parcours partiel de l'arbre de recherche, pour trouver de bonnes solutions (non nécessairement optimales). Au delà de 100 nœuds, il faut utiliser soit les méthodes linéaires (branch & cut) si l'on souhaite un résultat exact soit les techniques d'optimisation locale si l'on cherche seulement une bonne solution. 16
84 17
85 5. Chapitre 5. Problèmes de grilles d emplois du temps Ce chapitre traite d un sujet à la mode, mais beaucoup moins défriché que les précédents. Les problèmes d emplois du temps sont nombreux et variés. L état de l art n est pour l instant pas très établi. De nombreuses solutions ont été implémentées mais peu ont été comparées. A notre connaissance, aucune formulation du problème n a pour l instant vraiment fait l'objet d'un consensus pour le développement et la comparaison d algorithmes réutilisables. Ce sujet est donc beaucoup plus expérimental. Par manque de points de comparaison, on développera dans ce chapitre, de manière privilégiée, une résolution par contraintes. 5.1 Introduction, typologie Les problèmes d emplois du temps sont multiples et variés, leur point commun est de chercher à planifier l'activité d individus au cours du temps. Les solutions sont généralement visualisées sous forme de tableaux à deux dimensions dans lesquels on associe les lignes aux personnes et les colonnes aux périodes horaires. Mais les points communs aux problèmes d emplois du temps s arrêtent généralement là. En effet, la nature du problème peut sensiblement varier suivant plusieurs points de modélisation : 1. Le temps : celui-ci peut être modélisé de manière continue ou plus ou moins discrète. On peut chercher à affecter des activités pour des périodes d'une journée, ou bien chercher à contrôler de manière beaucoup plus fine l'emploi du temps du personnel, en planifiant un horaire à la demi-heure près, en tenant compte des pauses déjeuner, etc. 2. Les tâches affectées dans le plan. On peut modéliser de manière plus ou moins fine l'emploi du temps suivant que l'on cherche simplement à modéliser la simple présence d'employés, ou que l'on planifie de manière plus fine leur travail. Dans le premier cas, les possibilités d'activité se ramènent à une alternative binaire (travail - repos), dans le second, on peut considérer un nombre arbitraire d'activités possibles. 3. Les spécificités des personnes (qualification). Dans le cas, où l'on s'intéresse à la planification d'activités déterminées, on est amené à considérer les personnes de manière différenciée. On peut par exemple considérer que seules certaines personnes sont habilitées à faire une certaine tâche, ou bien que certaines sont capables d'assumer plus de travail que d'autres. La raison pour laquelle de nombreux problèmes font appel à la représentation commune sous forme de tableau est qu'elle permet aisément de résumer une situation à un instant donné, ou pour une personne donnée. Nous allons limiter cette étude à deux types de problèmes très -1différents, qui semblent représentatifs du spectre des problèmes d'emplois du temps, le problème d'allocation matricielle et le problème de gestion de personnel Le problème d'allocation matricielle Le premier problème étudié sera appelé le problème d'allocation matricielle. On considérera que les lignes du tableau correspondent à des individus et que les colonnes correspondent à des périodes de temps. On associera à chacune des cases du tableau une activité parmi un ensemble fixé à l'avance (on pourra créer une activité spéciale de repos pour les cases que l'on souhaiterait laisser vides). Cette modélisation permet généralement d'exprimer les problèmes de rotations d'équipes (par exemple pour les usines fonctionnant en 3/8 ou pour le planning d infirmières dans les hôpitaux)., dans lesquels les employés n'ont généralement pas d'emploi du temps fixe, mais prennent successivement un horaire ou un autre. 1
86 Lundi Mardi Mercredi Jeudi Vendredi Samedi Dimanche Judith M M S S N R R M = matin Dimitri N R M M R S S S = soir Jacques R R N N S M M N = nuit Isabelle S S R R M N N R = repos Figure 5-1: exemple de problème d'allocation matricielle pour un emploi du temps en 3/8 Dans la figure 5-1, on a considéré comme seules activités possibles des tranches horaires. On aurait pu tout aussi bien préciser de manière plus fine l'activité des personnels, en leur affectant des tâches précises. Les totaux par ligne et par colonne permettent, par exemple, de lire le nombre de jours de repos de Judith pour cette semaine ou bien le nombre de personnes présentes le lundi matin. Ces nombres ne sont généralement pas libres de prendre n importe quelles valeurs (tous les emplois du temps ne sont pas admissibles). On va considérer deux types de contraintes = Les totaux par colonne et par activité doivent généralement être au dessus de certains minima. Pour chaque activité a et chaque période de temps t, on peut ainsi considérer que l on veut qu'au moins n t,a employés soient affectés à l activité a à t. Ces nombres définissent pour chaque activité, la quantité de travail à traiter au cours du temps. = Les totaux par ligne et par activité doivent généralement être inférieurs à certains maxima pour respecter des contraintes de droit du travail ou des obligations syndicales (pas plus de 35 heures/semaine, pas plus de 3 nuits de travail en 2 semaines pour les 3/8, etc...). Les problèmes réels comportent généralement de nombreuses autres contraintes ad-hoc, par exemple en interdisant certaines séquences d'activités sur une ligne, ou en prenant en compte des impossibilités ou préférences personnelles, ou encore en synchronisant certaines activités entre différentes lignes (travail en équipe ou affinités personnelles), etc Le problème de gestion de personnel Le second problème auquel on s'intéresse, et que l'on appellera le problème de gestion de personnel, correspond à une version presque continue du problème d'emplois du temps où l'on affecte à chaque personne et à chaque jour, un horaire de travail. Cet horaire peut soit être un intervalle de temps (par exemple, 5 heures, de 14 h. à 19 h.), soit un horaire préétabli parmi une liste d'horaires définis à l'avance (par exemple 9 h h. puis 14 h h.). Le problème de gestion de personnel peut être vu comme : = une généralisation du problème d'allocation matricielle, puisque l'on met dans chaque case quotidienne un objet plus riche que dans le cas précédent. Dans ce cas, les contraintes de sommes verticales deviennent plus complexes. On ne demande plus seulement qu'il y ait un nombre minimal de personnes présentes chaque jour, mais on prévoit une courbe de charge de travail au cours de la journée. On cherche à rester le plus souvent possible au dessus de cette courbe (avoir, à chaque instant, suffisamment de personnel pour faire le travail), et à y coller au plus près, quand on est en dessous (c'est à dire, limiter ainsi la pénurie lors les périodes de sous-effectif). = une spécialisation du problème d'allocation matricielle, si l'on décompose le temps en périodes élémentaires (par exemple, en demi-heures). Dans ce cas, cela revient à augmenter sensiblement la taille de la grille (chaque colonne journalière se décompose en plusieurs dizaines de colonnes), mais chaque cellule élémentaire ne contient plus qu'une activité simple, indiquant la présence ou non de la personne. Pour ce type de représentation éclatée, il faut rajouter au problème pur d'allocation certaines contraintes sur les horaires construits, puisque n'importe quel ensemble de périodes élémentaires à l'intérieur d'une journée ne constitue pas nécessairement un horaire. Pour éviter les horaires à trous, 2
87 on rajoute certaines contraintes de connexité sur les ensembles de cellules d'une ligne où sont affectées des activités de présence et qui forment un horaire. (On est aussi souvent amené à imposer des contraintes plus complexes que la simple connexité pour prendre en compte les réglementations concernant la durée et la répartition des pauses, etc.) On mentionnera enfin qu'une troisième famille de problèmes, celle associée à la gestion des cours au lycée ou à l'université, peut se présenter comme une généralisation du problèmes des grilles, où l'on cherche à placer des objets complexes dans chacune des cases de la grille, pour prendre en compte la gestion d'autres ressources. Par exemple, un cours d'université peut être représenté par l'association d un groupe d élèves, d un professeur, d une salle, d un horaire et éventuellement de matériel pédagogique. On peut soit représenter le problème sur un tableau à plus de deux dimensions, soit garder le modèle à deux dimensions et introduire des contraintes de ressources limitées. 5.2 Un problème académique : l'allocation matricielle On commence par présenter le problème problème le plus simple, celui de l'allocation matricielle pure. On montre les liens entre ce problème et deux autres problèmes combinatoires, les multi-flots et la reconstruction de polyominos Formalisation Le problème d'allocation matricielle est une version simplifiée de problèmes d'emplois du temps, que l'on peut décrire de la manière suivante. On se donne une grille n m et un ensemble de k couleurs. On cherche alors à colorier chaque case de la grille d'une couleur, de sorte que : = pour chaque ligne i, (1 i n), le nombre de cellules de couleur c dans cette ligne i soit compris entre un nombre minimal Lmin i,c et un nombre maximal Lmax i,c. = on borne, de manière similaire, pour chaque colonne j, (1 j m), le nombre de cellules de couleur c dans la colonne j par Cmin j,c et Cmax j,c. On veut ensuite colorier chaque cellule de la grille de telle sorte que les contraintes de cardinalité sur les lignes et les colonnes soient respectées. Ce problème a été étudié [Ca 86] dans le cas où les totaux par lignes et par colonnes sont connus de manière exacte (Lmin i,c = Lmax i,c et Cmin j,c = Cmax j,c ). La procédure dans le cas k = 2 est simple, si l'on considère que les deux couleurs sont représentées par les entiers 0 et 1, on commence par produire une solution fractionnaire, en posant a ij = L i,1 C j,1 Σ avec Σ = L i,1 i = C j,1 j On transforme ensuite peu à peu la solution fractionnaire en une solution entière par additions matrices élémentaires de perturbation qui laissent les totaux par lignes et par colonnes invariants. +λ -λ -λ +λ Figure 5-2 : perturbation d'une solution d'allocation matricielle sans changement des projections 3
88 Ceci permet d'obtenir un algorithme polynomial pour k = 2. Le problème devient cependant NP-complet pour k 4 [Ca 86] et le statut du cas k = 3 est, à notre connaissance, encore ouvert Lien avec les multiflots On peut aussi traduire problème global d allocation matricielle de manière simple en un problème de multiflots (ou multi-commodity flows). Comme pour les flots, il s'agit toujours de faire passer un flux de matière à travers un réseau en respectant les équations de conservation de la matière à chaque noeud (lois de Kirchoff, voir [GM 95]), à ceci près que ce flux n'est plus uniforme mais est constitué de k produits différenciés: le flot n est ainsi plus représenté par un scalaire, mais par un vecteur à k coordonnées. En chaque noeud, on a k contraintes de conservation (le flux entrant égale le flux sortant pour chacun des k produits) et sur chaque arête on a une contrainte de capacité globale (la somme, en valeurs absolues, des composantes du flot doit être inférieure à la capacité totale de l'arête). En somme, un multi-flot est la superposition de k flots distincts. Si k = 1, le problème est un problème de flot, pour k > 1, les problèmes de multi-flots sont NP-complets [GM 95]. En particulier, les deux propriétés fortes des problèmes de flots (voir chapitre 2), le théorème des valeurs entières et le théorème de dualité entre le flot maximal et la coupe minimale ne sont plus valables dans le cadre des multi-flots. Pour une présentation détaillée de ces problèmes, on renvoie le lecteur à l'étude détaillée du chapitre 6 de [GM 95]. Le problème d'allocation matricielle peut être codé de manière naturelle en un problème de multi-flot. On considère un flux de dimension k, où k est le nombre de couleurs et on crée le réseau de taille nm + (k+1)(n+m) composé des nœuds suivants : = un nœud X ij pour chaque cellule (i,j) de la grille, = pour chaque couleur c, un nœud L i,c par ligne i et un nœud C j,c par colonne j, = un nœud global L i par ligne i et un nœud global C j par colonne j, = un nœud source global s et un nœud puits global t. On relie chaque nœud-ligne-couleur L i,c, par une arête de capacité Lmax i,c, au nœud ligne L i correspondant, et chaque nœud-colonne-couleur C j,c au nœud colonne C j correspondant, par une arête de capacité Cmax j,c. Ensuite, on relie chaque nœud-cellule X ij aux nœuds-ligne L i et nœuds-colonne C j correspondants, par des arêtes de capacité 1. Enfin, on relie s à tous les nœuds-ligne-couleur L i,c et t à tous les nœuds-colonne-couleur C j,c. On cherche alors un multiflot qui soit supérieur aux demandes suivantes : = on veut un flux d'au moins Lmin i,c entre chacune des sources L i,c et le puits global t, = on veut un flux d'au moins Cmin j,c entre la source globale s et chacun des puits C j,c. Il est alors aisé de vérifier que les multiflots entiers compatibles (satisfaisant les demandes) correspondent exactement aux allocations matricielles réalisables. 4
89 t L. Ma. Me. Elise X O Jean O X Gino X O X l,x l,o m,x m,o me,x me,o lundi mardi mercredi J, X J, O Jean J, lun J, mar J, mer s E, X E, O Elise E, lun E, mar E, mer G, X G, O Gino G, lun G, mar G, mer Figure 5-3: une grille d'allocation matricielle à deux activités (X et O) et le multiflot associé (traits continus par unité d'activité X et pointillés pour O) Modélisation par contraintes Le problème d'allocation matricielle peut aisément être exprimé dans la plupart des systèmes de contraintes sur les domaines finis grâce à des opérateurs permettant de compter le nombre d'occurrences d'une valeur dans une liste de variables. Par exemple, on dispose dans le système CHIP [vhd 91] des contraintes suivantes : occur(y,v,[x 1,..., X n ]) Y = card({i (1..n) X i =v}) atmost(n,[x 1,..., X n ],v) card({i (1..n) X i =v}) n atleast(n,[x 1,..., X n ],v) card({i (1..n) X i =v}) n En associant à chaque cellule de la grille d'emploi du temps une variable à valeurs dans l'ensemble des activités possibles, on peut ainsi modéliser complètement le problème de l'allocation matricielle. On va décrire la propagation de ces contraintes d'occurrences. On veut par exemple propager la contrainte Cmin j,c card({i, X ij = c}) Cmax j,c. Les deux inégalités sont propagées de manière différente. = Pour l'inégalité droite (contrainte atmost), on compte le nombre de variables (X ij ) qui sont déjà affectées à la valeur cible (c). Dès que ce compteur, Sure j,c, atteint la borne supérieure, on retire la valeur cible des domaines de toutes les autres variables non affectées (car toute affectation supplémentaire de cette valeur entraînerait un dépassement de la contrainte de cardinalité). = Pour l'inégalité gauche (contrainte atleast), on compte le nombre de variables (X ij ) qui peuvent prendre la valeur cible (c). Dès que ce compteur, Possible j,c, atteint la borne inférieure, on affecte toutes ces variables à la valeur cible (car si l'une de ces variables prenait une valeur autre, on passerait sous le seuil minimal de la contrainte de cardinalité). Ces deux règles de propagation suffisent à résoudre le problème d'allocation matricielle pure. Il suffit alors de développer un arbre de recherche basé sur l'affectation d'activités aux cellules. Nous verrons plus loin comment renforcer cette propagation, ainsi que comment construire un arbre de recherche efficace. 5
90 5.2.4 Application à la reconstruction de polyominos On s'intéresse maintenant à la résolution d'un problème combinatoire très proche de celui de l'allocation matricielle, bien que motivé par des applications différentes. Il s'agit de la reconstruction de polyominos sur une grille bidimensionnelle, à partir de leur projections. Considérons le cas bicolore du problème d'allocation matricielle (disons que l'on colorie les cellules de la grille, soit en blanc, soit en noir). L'ensemble des cellules noires définit alors une forme sur cette grille, appelée polyomino. Supposons encore que l'on connaisse, le nombre exact de cellules noires dans chaque ligne et chaque colonne (i.e. Lmin i,noir =Lmax i,noir et Cmin j,noir =Cmax j,noir ). Trouver une allocation revient à reconstruire le polyomino à partir de la donnée de ses projections suivant les deux axes. Les problèmes de reconstruction de polyominos se posent naturellement pour l'interprétation de données issues de scanners (par exemple, en imagerie médicale, pour visualiser de tumeurs, pour lesquelles on pratique des coupes à intervalles réguliers), mais aussi en compression de données ou en traitement d'image. Pour les applications de tomographie discrète, on est naturellement amené à rajouter certaines exigences sur le type de formes que l'on reconstruit. Par exemple, on peut chercher à reconstruire un polyomino 1. quelconque (allocation matricielle pure à deux couleurs), 2. connexe (on impose de toujours pouvoir aller d'une cellule du polyomino à une autre par un chemin de cases adjacentes par une arête), 3. "convexe par lignes" (i.e., dont chacune des lignes forme un ensemble convexe de cases), 4. convexe par lignes et par colonnes, et de plus, connexe. La figure suivante montre quatre polyominos représentatifs de ces quatre cas, tout en ayant les mêmes projections (projections horizontales : (1,4,4,4,4,1), projections verticales : (1,3,3,4,3,3,1) ). (1) (2) (3) (4) Figure 5-4: Quatre polyominos différents ayant les mêmes projections Le problème de reconstruction est de difficulté très variable suivant les conditions que l'on impose. Polynomial pour le premier cas (aucune condition) [Ry 63], le problème devient NP-complet, dès que l'on impose une des deux conditions de convexité [BdL 96], reste NP-complet avec une des deux conditions de convexité et la connexité, puis redevient polynomial dans le dernier cas (avec les deux conditions de convexité et celle de connexité) [BdL 96]. Le paragraphe suivant propose une alternative à l'algorithme polynomial complexe de [BdL 96] pour reconstruire des polyominos horizontalement et verticalement convexes ainsi que connexes d'après leurs projections. Le but recherché est double : d'une part, on voudrait proposer un algorithme plus simple a programmer que celui de [BdL 96], d'autre part, on voudrait tirer possibilité de la souplesse de l'approche par contraintes pour envisager le cas plus général de reconstruction au mieux d'un polyomino, à partir de projections peu précises Résolution des problèmes de polyominos La modélisation la plus simple pour le problème des polyominos est basée sur la formulation comme problème d'allocation matricielle. A chaque cellule (i,j), on associe une variable à valeur dans {0,1}, X ij 6
91 (la valeur 1 dénote la présence de la case dans le polyomino). Le problème peut alors s'exprimer par l'ensemble des contraintes linéaires suivantes : i, X j ij = L i, j, X ij i i, j, k,k L i, X ij + X i+k, j 1, i, j, k,k C j, X ij + X i, j+k 1, i, j X i, j X i +1, j 1 = C j, Les deux premières familles de contraintes consituent les contraintes habituelles de projection, les deux familles suivantes sont les contraintes de convexité par colonnes et par lignes, et la dernière famille de contrainte est une contrainte de connexité globale. Cette formulation peut d'ailleurs être rendue complètement linéaire (en changeant l'expression de la contraintes de connexité) et être enrichie par des contraintes redondantes, comme par exemple, ces contraintes linéaires impliquées par la convexité (plans de coupe) : i, j, X ij X i 1, j + X i+1, j 1 Nous allons maintenant proposer une autre modélisation utilisant des contraintes plus riches, et combinant variables Booléennes et variables entières. De la modélisation précédente, on garde les variables Booléennes X ij, et on introduit d'autres variables. Pour chaque ligne i, on créée les variables DébutLi et FinL i, à valeur dans {1,..., m}, et pour chaque colonne j, on créée les variables DébutC j et FinC j. Les contraintes de projection pourraient aisément être exprimées par une contrainte d'occurrences, qui compte le nombre de fois qu'une valeur peut être prise dans une liste de variables. i, Occurrences(1, (X i1,..., X im )) = L i On propose d'introduire une spécialisation de ces contraintes d'occurences, introduisant la contrainte : ConsectiveOccurrences(v, l, N, D, F) où, v est une valeur, l une liste de variables entières, et N, D et F trois variables entières. Cette contrainte impose qu'il y ait exactement N occurrences de la valeur v dans la liste de variables l, et que celles-ci représentent exactement l'ensemble des variables entre la position D et la position F (D et F comprises). La propagation par cohérence d'arcs de cette contrainte est simple à mettre en œuvre : on décrit ci-dessous la propagation de deux informations, quand une variable de la liste prend la valeur v, ou quand on sait qu'elle prend une autre valeur. l[i] = v { D i N +1, F i + N 1 j, (l[ j] = v) ( k (i.. j), l[k] = v) D [i N +1, i], F [i, i + N 1] l[i] v j, (l[ j] v j i N 1) ( k (i.. j), l[k] v) F D 2N 1 i [F N +1, D + N], l[i] = v Deux remarques peuvent être faites à propos de cette nouvelle contrainte. D'une part, on note qu'elle est la forme la plus simple de contraintes plus générales (qui restent encore à proposer) de reconnaissance de motifs parmi une séquence. On pourrait par exemple proposer des contraintes pour reconnaître des motifs décrits par des expressions régulières : on s'est ici contenté de reconnaître le motif 1 N (La succession de N fois la valeur 1). D'autre part, la troisième règle d'inférence présentée ci-dessus mérite quelques 7
92 explications. Si l'on considère que les variables D et F représentent des dates dénotant de le début et la fin d'exécution d'une tâche, cette règle revient à dire que dès que la fenêtre de temps d'une tâche est de largeur inférieure au double de la durée de la tâche, on peut prévoir qu'à certains instants (situés au milieu de la fenêtre de temps), la tâche sera en cours d'exécution, quelle que soit sa date de début. Ce raisonnement revient à comptabiliser les parties fixes des tâches (ici, des parties sûres du polyomino) et est une technique éprouvée en ordonnancement cumulatif ( 6.2.2). partie fixe durée Figure 5-5: propagation des parties fixes (cellules qui sont de manière certaine dans le polyomino) On va aussi avoir besoin d'une autre contrainte (plus simple celle-ci), affirmant qu'une séquence de valeurs est croissante. Cette contrainte se présentera sous la forme suivante : Increasing(l, D, F) i,j, (D i j F l[i] l[j]) On peut maintenant présenter la formulation complète par contraintes : i, ConsectiveOccurrences(1, (X i,1,..., X i,m ), L i, DébutL i, FinL i ) j, ConsectiveOccurrences(1, (X 1,j,..., X n,j ), C j, DébutC j, FinC j ) Decreasing( (DébutL 1,..., DébutL n ), 1, DébutC 1 ), Increasing ( (FinL 1,..., FinL n ), 1, DébutC m ) Increasing( (DébutL 1,..., DébutL n ), FinC 1, n ), Decreasing ( (FinL 1,..., FinL n ), FinC m, n) Decreasing( (DébutC 1,..., DébutC m ), 1, DébutL 1 ), Increasing( (FinC 1,..., FinC m ), 1, DébutL n ) Increasing( (DébutC 1,..., DébutC m ), FinL 1, m ), Decreasing( (FinC 1,..., FinC m ), FinL n, m) i, (DébutL i DébutL i+1 FinL i ) (DébutL i+1 DébutL i FinL i+1 ) Dans cette modélisation, les variables Booléennes sont ainsi uniquement utilisées pour synchroniser les contraintes d'occurrences et ainsi répercuter l'information des projections verticales sur celles horizontales. Les huit contraintes permettent de synchroniser les deux modèles (horizontal et vertical) par une analyse des quatre frontières (NW, NE, SW et SE) du polyomino. Enfin, la dernière contrainte assure la connexité du polyomino. L algorithme de recherche commence (comme dans [BdL 96]) par instancier les quatre pieds du polyomino (DébutL 0, DébutL n, DébutC 1, DébutC m ) pour ancrer le polyomino aux bords du cadre. Après ces quatre instantiations, le problème à résoudre ressemble à un problème 2-SAT, puisque le polyomino est déjà construit de manière relativement précise, et qu'en pratique, les seules contraintes qui restent actives sont les contraintes d'exclusion suivantes (impliquées par les contraintes ConsecutiveOccurrences). X i,j xor X i,j+li, X ij xor X i+ci,j Un problème 2-SAT comprenant N variables et M clauses, peut être résolu par un algorithme de parcours de graphe de complexité en O(M) [GJ 79]. L'algorithme naïf par contraintes (propagation simple en forward checking) a une complexité en O(MN). 8
93 Le tableau 5-1 présente des expériences sur quelques problèmes difficiles de reconstruction de polyominos. En pratique, on voit que l'algorithme permet de résoudre par simple propagation la plupart des problèmes de taille moyenne (jusqu'à ). taille du problème temps arbre de recherche [BdL 96] 150 ms. 4 nœuds [Ni 96] 70 ms. 0 nœuds ms. 0 nœuds s. 0 nœuds s. 0 nœuds s. 0 nœuds Tableau 5-1: Reconstruction de polyominos (temps sur SUN Sparc Ultra-1) Cette approche a été étendue au cas de projections connues avec incertitude. Dans ce cas, on cherche à reconstruire un objet dont les projections réelles sont les moins éloignées possibles des projections escomptées (en minimisant la distance L 1 entre les vecteurs de projections, par exemple). L'approche par contraintes qui a été décrite s'adapte aisément (les données L i et C j deviennent des variables au lieu d'être des constantes). On obtient dans ce cas un algorithme capable de reconstruire au mieux des polyominos à partir de projections erronées, pour des problèmes de taille (et un nombre minimal de défauts entre 5 et 15). Cet algorithme par contraintes est compétitif avec les algorithmes spécialisés développés pour les tomographes [GN 97], et bien plus efficace à qu'une résolution par contraintes utilisant un simple solveur Booléen [KBZ 97]. 5.3 Contraintes redondantes On propose dans cette partie quatre techniques pour améliorer la propagation lors de la résolution d'un problème de grille d'emplois du temps. Les deux premières sont applicables uniquement dans le cas d'une allocation matricielle pure : la première prend en compte l'ensemble des contraintes d'occurrences de manière globale, la seconde résout de manière très précise les sous-problèmes associés à une ligne ou une colonne. La troisième technique s'intéresse aux problèmes de gestion de personnel dans lesquels on affecte à chaque cellule un horaire sous forme d'intervalle de temps, et relaxe le problème d'affectation d'horaires au personnes (ce qui revient à considérer toutes les personnes comme indiscernables). La dernière technique permet de prendre en compte, toujours pour les problème de gestion de personnel, des préférences de la part des personnes sur les horaires alloués Cohérence matricielle globale La modélisation du problème d'allocation matricielle par une série de contraintes d'occurrences, telle qu'elle a été décrite plus haut, s'avère en pratique peu robuste. La raison est que la propagation des contraintes d'occurrence exploite peu la structure globale de la matrice. Par exemple, si l'on considère une grille hebdomadaire à sept colonnes (les jours) et huit lignes (personnes), et que l'on désire avoir, chaque jour, au moins sept personnes présentes, et si l'on exige au minimum un jour de repos par semaine par personne, le problème est insatisfiable. En effet, chaque jour, au plus une personne peut se reposer, ce qui ne permet pas aux 8 personnes de se reposer en 7 jours). L'algorithme de propagation que nous avons 9
94 décrit est incapable de détecter ce type d'impossibilités. Pour s'en aprecevoir, il faudrait combiner les contraintes d'occurrences sur les lignes et les colonnes. On va donc être obligé d'instancier de nombreuses variables avant d'aboutir à une contradiction, ce qui va amener au développement d'un arbre de recherche de taille conséquente. Pour éviter de telles situations, on va rajouter des contraintes redondantes prenant en compte la globalité des contraintes d'occurrences [CGL 95]. On introduit i (1.. n), L i,c = { j (1.. m) X ij = c} et j (1.. m), C j,c = { i (1.. n) X ij = c} On va être amené à raisonner sur la cardinalité maximale (resp. minimale) de ces ensembles, que l'on notera L inf i,c et L sup i,c. Ces bornes seront initialisées aux valeurs Lmin i,c, Lmax i,c, mais on pourra ensuite les affiner de manière dynamique, en cours de résolution (par exemple L inf i,c Sure i,c et L sup i,c Possible i,c ). La première contrainte redondante que l'on peut écrire consiste à sommer toutes les contraintes d'occurrences sur une même ligne (ou une même colonne). On a ainsi : i, card(l i,c ) c = m ce qui se propage par les règles suivantes inf c,i L i,c m sup Li,c' c' c sup c,i L i,c m inf Li,c' c' c (des contraintes similaires sont valables pour les bornes des sommes par colonne). La seconde contrainte consiste à remarquer que l'on peut compter le nombre total de cellules associées à une activité, en parcourant la matrice par lignes ou par colonnes. On obtient ainsi la deuxième contrainte redondante : c, card( L i i,c ) = card( C j j,c ) qui peut fournir d'une part des contraintes d'intégrité à vérifier c, inf C j j,c i sup L i,c et d'autre part, des règles de propagation pour mettre à jour les compteurs sup c, j, C j,c i sup Li,c k j inf C k,c inf C j,c inf Li,c i (les règles symétriques sont valables pour les bornes par lignes). De manière expérimentale, ces contraintes redondantes semblent fournir une bonne indication de la faisabilité du problème, pour le cas de l allocation matricielle pure [CGL 95] Modélisation du sous-problème associé à une colonne par des flots On va maintenant décrire une technique redondante pour l'allocation matricielle en résolvant de manière exacte les sous-problèmes associés à chacune des colonnes. En effet, on peut remarquer que, dans le cas où le nombre de chaque activité dans la colonne est connu, le problème d'allocation associé est presque une allocation bijective. On va ainsi utiliser un algorithme de flot, comme ceux évoqués dans le chapitre sur l'allocation de ressource (chapitre 2). Cette contrainte a été proposée par Régin [Ré 96]. On s'intéresse à la colonne j et on construit un graphe biparti dont les ensembles de sommets sont d une part les lignes (L i ) et d autre part les couleurs (K c ). On relie le sommet L i à tous les sommets K c tels que la couleur c puisse être affectée à la cellule (i,j). On relie chaque noeud L i au noeud source s et chaque k j sup C k,c 10
95 noeud K c au puits t par une arête de capacité maximale C sup c,j et de capacité minimale C inf c,j. On s'intéresse aux flots compatibles, c'est à dire aux flots qui font passer sur chaque arête une quantité de flot supérieure ou égale à la capacité minimale de l'arête). On peut alors vérifier que les flot compatibles maximaux (de flux n) dans ce graphe correspondent exactement aux allocations des cellules de la colonne j. La traduction du problème d allocation en termes de flots est presque la même que pour les couplages bipartis: on a simplement augmenté les capacités sur les arêtes partant de l ensemble des couleurs, pour autoriser une couleur à être affectée plusieurs fois dans la colonne. Cette modélisation est intéressante car elle permet d éliminer un certain nombre de possibilités à l avance: en effet, en détectant à l avance les arêtes n'appartenant à aucun flot maximal et qui ne seront donc jamais utilisées, on peut retirer des domaines les valeurs correspondantes. s Judith Dimitri Suzanne Virginie 6h-14h 14h-22h Repos 2 t Figure 5-6: l'allocation sur une colonne (une journée) correspond à un flot maximal (les arêtes Dim,AM et Dim,PM n'étant dans aucun flot maximal, on peut les supprimer) Cette propagation peut se faire de manière complète (c'est à dire que l'on peut éliminer tous les arcs n'appartenant à aucune solution) grâce à l'observation suivante [Ré 96] : Théorème : Soient f et f deux flots maximaux. alors f uv = f' uv si et seulement si l arête uv n appartient à aucun cycle propre du graphe d écart R(f). On peut donc chercher un flot maximal f dans le graphe biparti, et éliminer toutes les arêtes sur lesquelles le flot est nul et qui n appartiennent à aucun cycle propre du graphe d écart R(f). En effet, ces arêtes ne seront sélectionnées dans aucune allocation. Cette analyse permet d éliminer des arêtes inutiles par le simple calcul de flot compatible maximal et du graphe d écart. Cette technique de coupe peut être utilisée, en particulier lors de la phase de propagation initiale, avant le début de toute recherche arborescente. Bien qu un peu lourde (l'algorithme de calcul des composantes fortement connexes du graphe d'écart est de complexité linéaire en le nombre d'arêtes du graphe associé à la colonne) comparée à la simple propagation des contraintes de compatibilité, elle permet de réduire efficacement les domaines quand les lignes (personnes) sont assez différenciées. En revanche, quand beaucoup de couleurs (activités) sont disponibles pour chacune des cellules, cette modélisation redondante apporte peu Couverture de la courbe de charge par des intervalles On s'intéresse maintenant au cas de la gestion de personnel, quand on cherche à remplir chacune des cellules du tableau, non plus par une activité, mais par un intervalle de temps indiquant la plage horaire de présence de la personne lors de la journée. Dans ce cas, les contraintes de sommes verticales ont été remplacées par une modélisation plus fine par une courbe de charge, qui fixe, pour chaque journée (colonne), non plus un seuil minimal de présence cumulée, mais un profil minimal. Par exemple, si on a échantillonné le temps toutes les demi-heures, ceci revient à fixer un seuil minimal de présence demiheure par demi-heure. L'algorithme que nous allons présenter s'intéresse à la résolution du sous-problème associé à une colonne, et teste de manière exacte la satisfiabilité d'un problème relaxé, dans lequel on considère toutes les personnes de manière indifférenciée. On dispose d'un certain nombre d'horaires possibles, et on cherche, pour chaque type d'horaire admissible, un nombre d'horaires à sélectionner, de manière à couvrir au plus 11
96 près la courbe de charge. Ce problème relaxé peut aussi être vu comme la première phase d'une procédure de résolution en deux étapes : (sélection des horaires, puis affectation des horaires aux personnels). On va montrer que ce problème peut s'exprimer comme un problème de flots. Pour cela, on note x 1,..., x n, les points de discrétisation du temps et on note I 1,..., I m. les intervalles de temps correspondant à des horaires admissibles. Chaque intervalle I j est un horaire qui peut être affecté à M j des K personnes. Enfin, on suppose qu'entre deux instants consécutifs x i et x i+1, on veut au moins c i personnes présentes. On construit ensuite un réseau en prenant comme nœuds x 1,..., x n. On introduit une arête entre x i et x i+1 de capacité K-c i, pour chaque intervalle I j =[x p,x r ], on introduit un arc entre x p et x r de capacité M j. A tout flot entier de valeur K, on peut associer un ensemble d'intervalles qui couvre la courbe de charge. Le problème: affecter des horaires au personnel de manière à couvrir la courbe de charge Chargede travail: 8h - 9h: 10 9h - 10h: 19 10h - 11h: 27 11h - 12h: 23 12h - 13h: 11 13h - 14h: 13 14h - 15h: 16 15h - 16h: 19 16h - 17h: 21 17h - 18h: 14 18h - 19h: 5 19h - 20h: 3 personnel Personnel: 32 personnes dont les horarires possibles sont : 8h - 17h 9h - 12h 9h - 13h 9h - 18h 15h - 20h temps Une solution couvrant la charge avec 16 plein-temps et 16 mi-temps: 8h - 17h : 4 personnes 9h - 12h : 6 personnes 9h - 13h : 5 personnes 9h - 18h: 12 personnes 15h - 20h: 5 personnes personnel temps Le flot associé (les valeurs du flot sont indiquées le long des arcs) h 9h 12h 13h 15h 17h 18h 20h
97 Figure 5-7: Utilisation du flot pour couvrir une courbe de charge par un ensemble d'horaires Les flots maximaux (de valeur K) à valeurs entières dans ce réseau fournissent directement une sélection d'horaires couvrant la courbe de charge. Si (comme c'est souvent les cas), on ne cherche plus seulement une solution quelconque, mais celle qui couvre la courbe de charge avec le minimum de dépassements, on peut ajouter des coûts (proportionnels à la largeur des intervalles) à tous les arcs représentant des horaires de travail, et chercher dans le réseau un flot maximal de coût minimal. Le tableau 5-2 présente des expériences faites sur des problèmes réels (provenant d'une application de gestion de stock) publiés dans le cadre du projet CHIC-2 [CK 97]. On a transformé le problème de couverture de la courbe de charge en un problème de flot maximal de coût minimal, que l'on résout par l'algorithme de Busaker et Gowen [BG 61], décrit au chapitre 2. Pour chaque problème, on mentionne le nombre d'horaires différents qui peuvent être utilsés pour couvrir la courbe de charge (suivi, entre parenthèses, du nombre total d'horaires disponibles). Puis, on indique de la même manière (nombre d'horaires différents et nombre total d'horaires) le nombre d'horaires utilisés dans la solution optimale dans la solution optimale, la surcouverture de la solution optimale (le pourcentage de la surface qui dépasse de la courbe de charge) ainsi que le temps de résolution de l'algorithme de flot. Problème nombre d'horaires nombre d'horaires utilisés sur-couverture temps de résolution (SUN Sparc Ultra-1) B (366) 26 (116) 14.5% 1.2 s. B (3016) 40 (1240) 20.8% 2 s. B (6142) 29 (1788) 25.1% 1.9 s. B (6323) 31 (1274) 43.3% 7.6 s. B (8085) 88 (3082) 8.7% 43 s. Tableau 5-2 : Problème de couverture d'une courbe de charge résolu par un algorithme de flot maximal de coût minimal Prise en compte de préférences avec la méthode Hongroise La dernière contrainte redondante que nous allons décrire s'applique dans le cas de problèmes d'allocation matricielle avec préférences. On considère pour chaque cellule, un ensemble d'activités possibles (le domaine de valeurs de la variable associée à la cellule), et parmi ces activités, un sous-ensemble d'activités préférées. On cherche alors, non plus une allocation matricielle réalisable, mais l'allocation réalisable qui fera le plus grand nombre possible d'assignations préférées. Dans ce cas, on peut reprendre la modélisation en termes de flots pour l'allocation d'une colonne (paragraphe 5.3.2) et associer une pénalité (i.e. un coût strictement positif) aux arêtes correspondant à une allocation non préférée. On peut alors substituer à la recherche d'un flot maximal dans un graphe biparti celle d'un flot maximal de coût minimal, ce qui permet d'obtenir une borne inférieure du nombre de défauts de l'allocation dans la colonne (ligne) considérée. On peur alors utiliser la méthode Hongroise (décrite au chapitre 2) pour chercher et maintenir dynamiquement le coût du flot maximal de coût minimal. Cette approche a été appliquée avec succès à des problèmes d'emplois du temps scolaires [CL97b], représentés par le modèle de l'allocation matricielle avec des contraintes supplémentaires (chaque colonne 13
98 correspond à une heure, les cours de deux heures correspondent ainsi à l'affectation synchronisée de deux cases horaires). Le tableau 5-3 compare trois approches en P.P.C. : le premier algorithme propage de manière habituelle les contraintes de différences (dès qu'un cours est placé, on retire les cases horaire des domaines des autres cours). Le second algorithme considère le problème d'allocation matricielle (en coupant en morceaux les cours longs) et applique l'algorithme de propagation basé sur la recherche des flots maximaux [Ré 96] (décrit au 5.3.2). Enfin le troisième algorithme considère toujours le modèle d'allocation matricielle (cours de durée unitaire) et utilise la méthode Hongroise pour calculer une allocation maximale de coût minimal. Cette valeur minimale du coût est utilisée comme borne à l'intérieur d'un algorithme d'optimisation (branch & bound). Ces algorithmes sont comparés sur des petits problèmes (15 à 30 cours pour une classe sur une semaine -40h.-), pour lesquels le nombre minimal de professeurs mécontents de l'horaire de leur cours varie de 1 à 6. Problème 1 (24 cours) Problème 2 (26 cours) Problème 3 (14 cours) Propagation simple 189 kb. 1541s. 286 kb s. 43 kb. 246 s. Allocation matricielle propagation par flots Propagation du coût minimal de l'allocation 31 kb. 197 s. 269 kb s. 27 kb. 160 s b. 29 s. 234 b. 2.6 s. 17 kb. 120 s. Tableau 5-3 : application de la méthode Hongroise à la résolution de problèmes d'emplois du temps avec préférences 5.4 Application à des problèmes réels d'emplois du temps. On termine ce chapitre par la présentation de deux applications à des problèmes réels. Le premier est un petit problème, à temps discret, et comportant de nombreuses contraintes spécifiques. Le second est un problème de grande taille et à temps quasi-continu. On montrera comment utiliser le modèle d'allocation matricielle pour résoudre le premier problème ainsi que comment utiliser le modèle de gestion de personnel pour résoudre le second Allocation en grille La première application [CGL 95] décrit le fonctionnement d'un centre téléphonique en 3/8, du point de vue des chefs d'équipe. Le problème est de petite taille (un dizaine de personnes et un horizon de deux semaines). On cherche à affecter pour chaque journée une activité à chacune des personnes (travail le matin, l'après-midi, la nuit, repos, ou astreinte pour être prêt à remplacer un collègue défaillant). Le problème peut donc facilement être modélisé comme un problème d'allocation matricielle, d'autant que les nombres d'affectations par lignes et par colonnes doivent être compris entre certaines bornes. Ce cadre d'allocation matricielle doit être enrichi de contraintes supplémentaires typiques de telles applications réelles, et qui sont souvent des contraintes locales (entre quelques cellules connexes) et peuvent facilement être traitées à l'aide d'opérateurs de contraintes habituels. Par exemple, on peut vouloir interdire après une activité de nuit, une activité du matin pour la période suivante, ou imposer qu'après une activité fatigante, l'employé en fasse une plus reposante, ou encore imposer que les activités attribuées à chaque employé soient les mêmes le samedi et le dimanche, etc. Ces contraintes 14
99 d'incompatibilité décrivent exactement l'ensemble des activités admissibles le lendemain (ou la veille) d'une activité donnée. L'ensemble de ces contraintes ad-hoc justifie l'utilisation de la programmation par contraintes plutôt que les techniques de multi-flots, moins aptes à traiter de telles contraintes supplémentaires. L'approche simple par contraintes consiste à créer autant de variables que de cellules dans le tableau, leur associer à chacune un domaine (l ensemble des activités pouvant être affectées à cette cellule) et à construire un arbre de recherche basé sur l affectation de couleurs aux cellules. Les contraintes de compatibilité sont évidentes à propager: si on place une activité dans une cellule, on peut enlever du domaine des cellules adjacentes les activités incompatibles. On décrit maintenant un autre mode de résolution, plus robuste. L'idée est de d'abord chercher à placer des activités pour satisfaire en premier les contraintes de minima : pour chaque période de temps j et pour chaque activité c on crée ainsi Cmin j,c requêtes d affectation de l'activité c dans la colonne j. Le problème peut ainsi être vue de deux manières duales, soit par affectation de cellules, soit par placement de requêtes dans la grille. Une des leçons du problème des n reines [Jou 95] est qu en présence de modélisations multiples, il vaut mieux raisonner dans les deux modèles à la fois et poser dynamiquement des points de choix dans l un et l autre, car la modélisation multiple permet de mieux se concentrer sur les goulots d étranglement (et est donc particulièrement efficace pour élaguer les branches insatisfiables). Reste ensuite à connecter les modèles en propageant les choix de l un dans l autre : ici, on supprimera une requête à chaque affectation de cellule et on affectera une cellule à chaque placement de requête. Reste enfin à choisir le point de choix. Pour le modèles d affectation de cellules, le principe du first-fail paraît être un choix naturel. Parmi les cellules non affectées, on choisira donc en priorité celles qui ont le plus faible nombre d activités possibles. Le choix d'une requête est plus délicat, on procédera à un choix entropique, c est à dire un choix dont la propagation a beaucoup d'impact et diminue fortement le nombre de degrés de liberté du système, comme pour la stratégie d'edge-finding décrite au chapitre 3 : dans le cadre de l'ordonnancement, on avait utilisé les marges des fenêtres de temps des tâches pour évaluer l'entropie du système, ici, on utilisera les relations d'incompatibilité entre tâches adjacentes pour cette évaluation. Ainsi, on peut associer à chaque requête (pour une activité dans une colonne), une valeur représentant la facilité à placer la requête. On peut lier ces valeurs entre elles par des dépendances issues des relations de compatibilité. Un tel cadre heuristique est décrit dans [CGL 95] qui permet de caractériser les requêtes les plus difficiles à placer. Le schéma de branchement global doit ensuite équilibrer les deux modèles et choisir si le branchement se fait sur une requête ou sur une cellule. Pour cela, on utilise une fonction d'agrégation qui sélectionne une requête d'entropie minimale s'il en existe une sous un certain seuil, et sélectionne une cellule par le critère first-fail autrement. Ici encore, le choix d'une fonction d'agrégation à seuil, est une technique qui a déjà été utilisée dans le cadre de l'allocation de ressources (chapitre 2) et de l'ordonnancement disjonctif (chapitre 3). L ensemble de ces techniques (structure globale des compteurs, modélisation multiple et évaluation entropique des requêtes) permet de résoudre en quelques minutes des problèmes à plusieurs centaines de cellules, alors que la propagation de contraintes naïve est limitée à quelques dizaines de cellules Résolution du problème de gestion de personnel On aborde maintenant une dernière application, liée à la flexibilité de l'organisation du travail (possibilités d'horaires de durée variable, de temps partiel, etc.). Le but de l'entreprise est d'utiliser au mieux cette flexibilité des horaires pour suivre au plus près une certaine courbe de charge. On considérera un problème d'une centaine de personnes sur une à deux semaines, chaque journée étant découpée en une trentaine de périodes élémentaires (découpage par 20 minutes). On ne distinguera pas 15
100 plusieurs activités, on disposera simplement pour chaque jour, d'une courbe de charge théorique représentant l'estimation a priori de la quantité de travail à accomplir, que l'on essaiera de respecter au plus près (mais sans que cette contrainte de charge doive être absolument respectée de manière rigide). Les horaires possibles seront soumis à de multiples contraintes (pour éviter les trous, pour éviter de faire travailler les gens sur des périodes trop courtes, etc.). Enfin, on considérera que les employés ont des périodes de disponibilité différentes. La technique habituelle pour résoudre de tels problèmes consiste à décomposer la résolution en deux étapes [Pa 97]. La première commence par générer tous les horaires légaux possibles sur l'ensemble de la période de temps (la semaine), et choisit le nombre de chaque horaire à sélectionner, de manière à approcher la courbe de charge au plus près : Cette première phase de couverture de la courbe de charge, appelée rostering, est généralement faite par des techniques de flots ou de programmation linéaire. Ensuite, une seconde phase consiste à affecter les horaires aux employés en tenant compte des contraintes additionnelles, ainsi que d'éventuelles préférences. Cette seconde phase est traditionnellement problématique, puisqu'il n'est généralement pas possible d'affecter les horaires fournis par la première phase, sans violer certaines contraintes. On est donc amené à reformuler le problème autrement, éventuellement en surestimant la charge lors de la première phase, pour obtenir une situation admettant une solution pour la seconde phase. Cette deuxième étape fait parfois appel à des techniques de propagation par contraintes, mais ne peut généralement pas prendre en compte les spécificités individuelles. De plus, le fait de traiter séparément le problème de couverture et d'affectation conduit généralement à des pertes de qualité importantes. Nous proposons une approche intégrée pour résoudre les deux problèmes (couverture de la courbe de charge et affectation des horaires) en même temps. On va cependant résoudre séparément les problèmes associés à chaque journée. On commence par répartir les jours de travail et ceux de repos pour chacun des employés, de manière à affecter à chaque journée un nombre d'employés proportionnel à la quantité de travail par jour. Cette première phase peut se faire en tenant compte de contraintes de préférences et de constitution d'équipes. On choisit un algorithme heuristique quelconque. Le but de cette répartition a priori est d'éviter qu'en situation de manque (si l'on ne couvre pas tout à fait la courbe de charge, ce qui est souvent un cas réaliste), on évite de concentrer toutes les ressources sur les premiers jours de la période (les premiers problèmes résolus) pour couvrir complètement la demande et de se retrouver avec une couverture très partielle en fin de semaine. On préfère répartir le manque de manière équitable, tout au long de la période. Pour chaque journée, on se base sur un modèle d'allocation matricielle en créant des requêtes pour chacune des petites périodes de temps (il y en a donc plusieurs dizaines de milliers). On choisit ensuite d affecter au fur et à mesure les requêtes aux employés. Ceci permet de réduire au fur et à mesure l ensemble des horaires possibles pour un employé, sans pour autant figer ce choix avant l affectation. Retarder ce choix permet ainsi de propager les décisions d affectation sur le découpage en horaires. Même limité à une seule journée, le problème de gestion de personnel est de grande taille, et il est illusoire de vouloir le résoudre de façon exacte. On se contente juste d'une heuristique gloutonne, correspondant à la descente le long d'une branche d'un arbre de recherche, sans aucun retour arrière et qui construit la solution par petits morceaux. Un des points intéressants est la gestion de l'ensemble des horaires possibles pour chaque personne. Plutôt que de représenter cet ensemble en extension par un domaine, il peut être intéressant, si les contraintes spécifiques à l'application le permettent, de décrire cet ensemble en intention par un objet et ses champs (par exemple la date de début et la date de fin, la présence d'une pause, la présence d'un repas, leurs durées, leurs dates de début, etc.). La formalisation compacte d'un ensemble de possibilités d'horaires par un seul objet avec quelques champs permet de proposer de manière plus aisée des règles de propagation. Par exemple, on retrouve des règles de propagation comme celles liées à la convexité pour les 16
101 polyominos, inférant que si deux cellules sont sélectionnées, toutes celles entre elles le sont aussi. Par exemple, pour un horaire sans pause comportant une cellule de 8h à 8h20 et une de 10h40 à 11h, on peut déduire que toutes les cellules entre 8h et 11h sont dans l'horaire. De même si les contraintes interdisent de travailler 4 heures d'affilée sans pause, on peut déduire qu'un horaire comportant les cellules 13h- 13h20 et 18h40-19h, comporte nécessairement une pause commençant après 15h et finissant avant 18h. Le dernier point important de cet algorithme vise à le rendre plus robuste aux mauvaises décisions. Nous avons décrit un algorithme glouton qui affecte des requêtes les unes après les autres, sans jamais revenir en arrière sur un choix. Pour essayer de défaire des décisions qui s'avèrent, après coup, peu judicieuses, on choisit de faire un petit peu d'optimisation locale après chaque décision. Le principe de réoptimiser localement les solutions partielles, à chaque étape de l'algorithme glouton est un principe général, qui sera développé en détail pour le problème des tournées dans le chapitre 7. Pour le cas du problème de gestion de personnel, on considère deux types de mouvements locaux, présentés sur la figure 5-8. Le premier consiste, après affectation d'une requête sur le planning d'un employé, à chercher ou boucher les trous en scindant un autre horaire et en collant une des deux parties à l'horaire dans lequel on vient de faire l'insertion. Le deuxième mouvement consiste à essayer de libérer de la place aux endroits intéressants : quand une insertion semble bonne, mais est irréalisable car l'horaire de l'employé concerné est déjà surchargé (ayant atteint l horaire journalier maximal), on essaye de déplacer une partie des cellules de l'horaire de cette ligne intéressante vers un autre horaire, et en cas de succès dans le transfert, on autorise l'insertion devenue réalisable. insertion sortie de l'horaire Insertion illégale Figure 5-8: Deux mouvements d'optimisation locale (optimisation après une insertion et changement avant une insertion pour rendre légale une insertion irréalisable prometteuse) 5.5 Conclusion Dans ce chapitre, nous nous sommes attachés à la résolution de problèmes d'emplois du temps. Comme ce problème est moins défriché que d'autres, il se prêtait moins à des comparaisons avec d'autres approches : et nous nous sommes donc principalement cantonnés à une approche en programmation par contraintes. Après avoir évoqué la variété des problèmes d'emplois du temps, nous avons proposé un formalisme simple d'allocation matricielle pour décrire le problème de base, au cœur de nombreuses applications. Nous avons relié ce problème à la théorie des multiflots et nous avons présenté sa résolution en programmation par contraintes. Nous avons décrit un cas particulier (le problème de la reconstruction de polyominos) dans lequel on interprète de manière géométrique la grille d'emploi du temps. Pour ce problème-ci, l'algorithme de P.P.C. permet de résoudre de manière efficace les problèmes approchés (quand on connait avec imprécision les totaux par lignes et par colonnes). Dans le cas de l'allocation matricielle générale, pour rendre l'algorithme de P.P.C. plus robuste, nous avons décrit deux contraintes globales redondantes (une relation de consistance globale et une contrainte basée sur l'expression du sousproblème associé à une colonne). Nous avons ensuite proposé deux autres contraintes redondantes, applicables dans des cas spécifiques, la première s'appliquant dans le cadre d'un problème où l'on 17
102 subdivise encore le temps à l'intérieur de chacune des colonnes (pour des problèmes de gestion de personnel), et la seconde, lorsque l'on cherche une allocation matricielle qui satisfasse au mieux certaines préférences. Enfin, nous avons décrit deux applications réelles, l'une utilisant la modélisation de l'allocation matricielle pour planifier l'activité de 10 personnes sur 15 jours, et l'autre utilisant la modélisation du problème de gestion de personnel, pour planifier l'activité d'une centaine de personnes sur 10 jours. Les techniques complètes de programmation par contraintes présentées permettent de résoudre exactement des problèmes d'allocation matricielle comportant une centaine de cellules. Les techniques approchées, par insertion et optimisation locale permettent de résoudre de manière approchée, et avec une bonne qualité de résultat, des problèmes comportant un millier de cellules, à l'intérieur desquelles on gère le temps de manière fine. 18
103 19
104 6. Chapitre 6. Ordonnancement cumulatif Les problèmes d'ordonnancement cumulatif sont une généralisation des problèmes d'ordonnancement disjonctif au cas de ressources plus complexes, qui permettent de décrire de manière fidèle de nombreux processus industriels. Dans ce chapitre, on étudiera le cas général de l'ordonnancement cumulatif ainsi que sa restriction au problème RCPSP. On reprendra dans ce chapitre une partie du travail publié dans [CL 96b] et [CL 97c]. Comme l'ordonnancement cumulatif est un problème peu étudié en dehors des approches de type programmation par contraintes, on se limitera à l'étude de celles-ci. On montrera comment adapter les techniques d'edge-finding au cas cumulatif, et en particulier, celles liées au modèle des intervalles de tâches. On présentera d'autres techniques liées à une résolution par contraintes, comme les histogrammes de ressource, les règles de dominance ou le raisonnement énergétique. On illustrera leur utilisation sur trois types problèmes, l'ordonnancement cumulatif à une ou plusieurs ressources et le RCPSP. 6.1 Ordonnancement et ressources Dans les problèmes d ordonnancement auxquels on s'intéresse dans ce chapitre, les ressources ne sont plus disjonctives, mais peuvent effectuer plusieurs tâches en même temps. Ceci permet par exemple de modéliser des machines disponibles en plusieurs exemplaires ou encore le travail d une équipe. Le problème de l ordre d exécution des tâches peut alors se doubler d un problème d affectation de ressource : par exemple, un travail en équipe nécessitant 10 journées.hommes peut être fait par 5 personnes pendant 2 jours, ou à 2 personnes en 5 jours Typologie, Applications Formellement, on se donne, comme dans le cas de l'ordonnancement disjonctif, un ensemble de tâches t T et un ensemble de ressources r R, chacune disponible en une certaine quantité entière available(r) (on modélise donc le cas de k machines identiques par une ressource globale disponible en quantité k). Les tâches sont reliées entre elles par des contraintes de précédence imposant que certaines aient fini de s'exécuter avant que d'autres puissent commencer. Chaque tâche t a deux caractéristiques: la durée d(t), et les quantités des ressources qu elle occupe use(t,r). Si l on représente le projet sur un diagramme à deux dimensions (temps et consommation), chaque tâche est représentée par un rectangle. Le problème à une ressource est alors un problème de placement de rectangles dans une bande horizontale (strip packing). ressource t1 t4 t7 t2 t3 t5 t6 t8 temps Figure 6-1: diagramme d'un ordonnancement cumulatif La version la plus simple de ce problème d'ordonnancement mono-ressource dans laquelle, on fixe les valeurs des durées et des niveaux de consommation des tâches, et où l'on cherche à optimiser la date de fin du projet, est déjà un problème NP-complet [Ka 72]. 1
105 Le problème cumulatif général ne fixe pas les valeurs des durées et des niveaux de consommation des tâches, mais simplement leur produit sur chacune des ressources, c'est à dire la quantité de travail nécessaire à l'accomplissement de la tâche (d(t) use(t,r) = size(t,r)). De plus, on fixe aussi une borne supérieure et une borne inférieure sur les niveaux de consommation admissibles (dans la pratique, quand il s'agit de travail en équipe, les niveaux inférieurs proviennent généralement de contraintes de sécurité et les niveaux supérieurs proviennent de contraintes de capacité, par exemple, d'encombrement physique). Remarquons que dans l'analogie graphique du placement de rectangles présentée dans la figure ci-dessus, le problème cumulatif revient à fixer la surface des rectangles ainsi qu'éventuellement une hauteur minimale et maximale. Une solution au problème cumulatif est ainsi constituée de dates de début, dates de fin et niveaux de consommation des tâches {start(t), end(t), use(t,r) t T, r R}, à valeurs entières et de sorte que : 1. les niveaux de consommation des tâches sont respectés : t T, r R, minuse(t,r) use(t,r) maxuse(t,r) 2. les quantités de travail des tâches sur chacune des ressources sont respectées : t T, r R, (end(t) - start(t)) use(t,r) = size(t,r) 3. à tout instant, sur chacune des ressources, les demandes n'excèdent pas la capacité globale de celle-ci : r R, x, use(t,r) available(r) { t T start(t ) x<end(t )} 4. les contraintes de précédences sont respectées : P(t 1, t 2 ) start(t 2 ) end(t 1 ) Enfin, on mentionnera certaines généralisations. D'une part, on peut accorder plus de flexibilité aux tâches, en autorisant leur consommation de ressource à varier au cours du temps. Ceci amène à remplacer les rectangles par des formes plus libres. On peut alors considérer ce nouveau problème comme une relaxation semi-préemptive du problème d'ordonnancement, puisque de telles tâches peuvent être décomposées en une partie ininterruptible à consommation fixée (le niveau minimal de consommation) et un ensemble de petites tâches interruptibles (préemptives). Le cadre de propagation que nous allons décrire a été conçu pour résoudre aussi de tels problèmes. Hormis le cadre que nous proposons, très peu de systèmes de contraintes permettent actuellement de résoudre de tels problèmes; le système le plus avancé est la librairie CLAIRE SCHEDULE [LB 96]. = + Figure 6-2: décomposition d'un tâche semi-préemtive en 3 tâches préemptives et une tâche fixe On mentionne enfin deux autres possibilités de généralisation. 1. D'une part, on peut introduire des contraintes de précédences complexes (correspondant à des situations réelles de processus de fabrication), imposant qu'une tâche ne puisse commencer qu'après qu'une proportion d'une autre (80%, par exemple) soit terminée. De telles contraintes peuvent être 2
106 traitées dans le cas cumulatif simple comme des précédences avec délai; dans le cas (semi-)préemptif, elles deviennent plus difficiles à propager. 2. D'autre part, si l'on chercher à optimiser le makespan quand le dimensionnement des ressources est connu, on peut aussi chercher à optimiser un compromis entre la quantité de ressource et l'horizon (makespan). On parle dans ce cas d optimisation pareto. Une solution (pour une ressource) est paretooptimale [Got 93] s il n existe ni une solution avec la même quantité de ressource et un horizon plus court (critère habituel), ni une solution avec le même horizon et une quantité de ressource plus faible RCPSP, modélisation linéaire On décrit maintenant un problème simplifié, le RCPSP (pour Resource Constrained Project Scheduling Problem), qui a été étudié par la communauté de Recherche Opérationnelle (cf. [De 92] pour une présentation générale des techniques de résolution). Pour cette classe de problèmes, on considère que les tâches sont de durées et de niveaux de consommation connus. Le problème peut être décrit de la manière suivante en programmation linéaire en nombre entiers. On introduit les variables x t,i à valeurs dans {0,1}, avec la signification suivante : x t,i = 1 end(t) = i On introduit aussi une borne supérieure M pour l'horizon du projet ainsi qu'une tâche supplémentaire last, de durée nulle et liée à toutes les autres par une contrainte de précédence. Le problème peut alors s'exprimer de la façon suivante. On cherche à minimiser : M i.x last,i i =0 sous les contraintes : M t T, x t,i =1 i =0 t 1,t 2 T, P(t 1,t 2 ) i.x t2,i i =0 i [0, M], r R, M M d(t 2 ) i.x t1,i i =0 i+d(t) 1 use(t,r).x t T t,i j =i available(r) Cette modélisation linéaire est donnée ici à titre illustratif, car le très grand nombre d'inégalités qu'elle comporte la rend en pratique inutilisable pour des problèmes de taille intéressante. Ainsi que l'indique Demeulemeester [De 92], pour l'instant, les stratégies d'énumération implicite (auxquelles appartient la programmation par contraintes) sont les méthodes les plus efficaces. 6.2 Techniques de propagation de contraintes La résolution par contraintes des problèmes cumulatifs reprendra la modélisation de l'ordonnancement disjonctif. On définira ainsi pour chaque tâche t : = la date de début au plus tôt t, et la date de fin au plus tard t, qui définissent une fenêtre de temps [t,t] à l'intérieur de laquelle la tâche t doit être exécutée = on notera minuse(t,r) et maxuse(t,r) les bornes inférieures et supérieures du domaine de la variable use(t,r), 3
107 = enfin, on notera span(t) la borne inférieure de la durée d(t). On aura ainsi, pour toute ressource r, span(t) maxuse(t,r) = size(t,r) La propagation des contraintes de précédence se fait de manière naturelle avec ces fenêtres de temps (il s'agit de la propagation usuelle de contraintes linéaires). P(t 1,t 2 ) t 2 t 1 +span(t 1 ) t 1 t 2 span(t 2 ) Cette règle de propagation sera déclenchée à chaque modification d'une borne de la fenêtre de temps ainsi que lors de modification de la durée minimale de la tâche. La propagation des contraintes de ressources est plus délicate: nous allons décrire d'un part les techniques d edge-finding (les règles d'inférences permettant de raisonner sur l'ordre relatif d'une tâche et d'un ensemble de tâches contribuant à une ressource commune) dans le cadre des intervalles de tâches, d'autre part, nous présenterons l utilisation d histogrammes de ressources pour affiner la propagation Intervalles de tâches Nous proposons le modèle des intervalles de tâches pour propager les contraintes de ressources. D'autres cadres similaires de propagation ont été proposés ([LP 94], [Nu 94], [NA 96], [BL 96], [Lo 96]). La grande différence entre le cadre de propagation que nous proposons, réside, en outre des inférences originales que nous proposons, dans la manière dont ces inférences sont faites. Alors que tous les cadres de propagation (comme [Nu 94]) font de manière globale en une passe l'ensemble des déductions sur une ressource, le modèle des intervalles de tâches déclenche ces règles par des démons. Ceci constitue donc une implémentation beaucoup plus simple (on s'appuie sur le compilateur de règles Marie [Ca 87], fourni avec le langage CLAIRE, pour produire les démons à partir de l'expression logique des règles), et plus réactive des règles, souvent au prix d'une exécution un peu plus longue. On rappelle que l'idée des intervalles de tâches (voir 3.2.3) est de considérer certains ensembles de tâches partageant une même ressource, et de prendre en compte la contrainte de ressource sur cet ensemble de manière globale. Ils ont été introduits pour l ordonnancement disjonctif et permettent de réduire les fenêtres de temps des tâches [CL 94],[CL95b]. On rappelle que la force de ce modèle est de posséder un algorithme de mise à jour incrémental, qui permet de recalculer les caractéristiques des intervalles après chaque modification d'une fenêtre de temps en O(n 3 ). Ce modèle s applique aussi bien au cas cumulatif si l on raisonne sur les surfaces au lieu des durées [CL96b][CL97c]. La définition des intervalles reste la même que précédemment : Définition : Soient t 1, t 2 deux tâches (éventuellement t 1 =t 2 ) vérifiant t 1 t 2 t 1 t 2 alors on définit l intervalle de tâches [t 1, t 2 ] comme l'ensemble suivant : [ t 1,t 2 ]= t 1 t t t 2 { } On généralise de manière naturelle la définition de la taille à un intervalle, en posant : r R, size(i,r):= size(t,r) t I Ces intervalles peuvent être utilisés de deux manières: pour détecter des situations impossibles et pour réduire les fenêtres de temps. Une première contrainte d intégrité assure, comme dans le cas disjonctif, que pour tout intervalle I la fenêtre de temps de I est suffisante pour y loger la surface totale de l ensemble de tâches, sur chacune des ressources. Cette contraintes d'intégrité est vérifiée par tous les algorithmes habituels d'edge-finding (comme ceux de [Nu 94], [NA 96], [BL 97]) : r R, size(i, r) ( I I) available(r) 4
108 Une deuxième règle (edge-finding) réduit les fenêtres de temps en détectant l'ordre relatif d'une tâche et d'un intervalle. La règle détecte ainsi des situations où une tâche ne peut pas commencer avant un intervalle, et dans ce cas, repousse la tâche vers la droite dans le diagramme (la première des deux conclusions de la règle). let slack := (I I) available(r) size(i,r), ( span(t) max(0,i t) ) use(t,r) > slack minuse(t,r) (I I) > slack ο ( ) ( ) οt := max t, I slack / minuse(t,r) ο ο t', P(t,t') t' := max t', I slack / maxuse(t,r) + span(t) Ce raisonnement est illustré sur la figure 6.3 : on remarque que t ne peut commencer avant I={u,v,w}, et donc, on en déduit que t commence au plus tôt en 6. Ce type de déduction constitue le cœur d'un système de propagation de contraintes de ressources, des mises à jours un peu plus fines sont mêmes proposées dans [Nu 94]. Pour une comparaison détaillée de l'efficacité de tels algorithmes, nous renvoyons le lecteur à [BL 96]. La seconde des deux déductions de cette règle est beaucoup plus originale, car elle combine les deux déductions t se termine après la fin de I et t' commence après la fin de t. Quand ces deux règles sont propagées séparément, on considère dans un cas (edge-finding) la consommation minimale et dans l'autre cas (précédence), la consommation maximale pour t. La prise en compte globale de ces deux déductions permet d'inférer une conclusion plus forte. tâche fenêtre temporelle surface u [2,8] 4 v [2,8] 6 w [2,8] 12 t [1,15] 6 t' [3,17] P(t,t') t S={u,v,w} L'intervalle S={u,v,w} a une marge de 2 unités libres. Si t commence avant S, t empiète d'au moins 3 unités sur la fenetre de S. Donc t commence au plus tôt à 6 et t finit au plus tôt à 10, donc t' commence au plus tôt à 10 Figure 6-3: Propagation des fenêtres de temps (règle d'edge-finding) La règle suivante, bien qu'elle ne soit pas impliquée par des raisonnements basés sur la surface des tâches, est une technique habituelle d'ordonnancement, qui a été utilisée depuis longtemps [ELT 91], [Lo 96]. Cette règle détecte que certaines tâches ne peuvent pas être effectuées en même temps parce que leurs 5
109 demandes en ressource sont trop importantes. Dans ces cas, on peut appliquer un raisonnement disjonctif : ( r R, use(t 1,r) + use(t 2,r) > available(r)) (t 1 + span(t 1 ) > t 2 span(t 2 )) P(t 2,t 1 ) Enfin, nous décrivons deux règles originales. La première est inspirée d'une règle d'inférence en ordonnancement disjonctif qui permet de déduire qu'une tâche a lieu après tout un intervalle parce qu'elle est après chacune des tâches individuelles de l'intervalle, soit à cause d'une précédence explicite, soit pour une raison liée aux fenêtres de temps. Ainsi, en dénotant P* la fermeture transitive de la relation de précédence P, on a : t' I, (t' t) P * (t',t) ( ) t := max t, I + size(i,r)/ available(r) ( ) La dernière règle de propagation, originale elle aussi, consiste en une contrainte d'intégrité spécifique à la procédure de construction globale adoptée. On va en effet développer un arbre de recherche en construisant l'ordonnancement de manière chronologique. Quand l'ordonnancement est construit jusqu'à l'instant s, on peut vérifier une relation de cohérence pour tous les semi-intervalles de tâches (s,t] (de manière intuitive, un semi-intervalle de tâche (s,t] est obtenu à partir d'un intervalle [t 1, t 2 ] commençant avant s, en enlevant toutes les parties de tâches qui ont lieu avant s. Formellement, ceci revient à vérifier que : r R, I t.q. I s < I size(t,r) max(0,(min(t,s) t)) maxuse(t,r) t I available(r) (I s) Si l'on compare l'algorithme réactif à base de règles que nous venons de présenter à des algorithmes de propagation impératifs implémentant les règles d'edge-finding, on peut noter deux différences : l'algorithme réactif a une complexité plus grande (mise à jour des fenêtres de temps des tâches sur une resource en O(n 3 ) au lieu de O(n 2 ), où n est le nombre de tâches utilisant la ressource). Cette complexité plus forte est compensée par un pouvoir de déduction plus fort (toutes les règles présentées ici font des déductions importantes). Ainsi, l'algorithme à base de règles offre le mérite d'être simple à décrire et facile à implémenter dans un environnment permettant d'écrire de telles règles. On pourrait chercher à encore améliorer l'approche en incorporant les règles de propagations originales proposées ici dans un algorithme de propagation quadratique. C'est ce que nous avons fait en 1996, en proposant un algorithme de complexité quadratique, basé sur celui de Martin et Shmoys [MS 96] pour l'ordonnancement disjonctif, et enrichi avec une partie des règles décrites ici. Le résultat a été décevant : nous avons passé longtemps à mettre au point un algorithme très complexe avec de nombreux calculs incrémentaux, qui n'a en fin de compte apporté aucun gain de rapidité sur les problèmes étudiés comportant une trentaine de tâches par ressource Parties fixes Les règles de propagation présentées dans les paragraphes précédents fondent leurs inférences sur une analyse des fenêtres de temps et permettent ainsi de vérifier que de manière globale, il y a toujours suffisamment de ressource disponible pour l ensemble des tâches. Cependant, elles ne permettent pas de repérer les conflits de ressource localisés, dus à des pics ponctuels de demande de ressource dans l'ordonnancement. Pour cela, on introduit la notion de parties fixes des tâches (déjà évoquée pour les polyominos au ). On peut en effet remarquer que pour toute tâche t : t span(t) t + span(t) [t span(t), t + span(t)] [start(t), end(t)] 6
110 Ainsi, dès que les fenêtres de temps des tâches deviennent suffisamment serrées, on peut comptabiliser comme étant sûres, certaines consommations de ressource au cours du temps. On va tabuler ces parties fixes dans un histogramme de consommation level tel que pour tout instant x [0, M], level(r)[x] := requires(t, r, x) t T requires(t,r, x):= minuse(t,r) si x [t span(t), t + span(t )+1] 0 sinon On peut ainsi vérifier à tout instant x, que l histogramme reste toujours sous le niveau available(r) et, en cas de dépassement, détecter une impossibilité. Cet histogramme peut être implémenté de multiples manières (voir [LP 94]): par un tableau ou par un arbre d intervalles, de manière exacte ou approchée (le cas échéant, avec un échantillonnage plus ou moins fin du temps). On peut aussi se servir de l histogramme de consommation pour faire de la propagation sur les fenêtres de temps. Ainsi, pour chaque tâche t sur la ressource r, on peut considérer les pics de l histogramme level(r) (auquel on a enlevé la contribution t). Quand un tel pic devient trop haut (si level(r)[x] - requires(t,r,x) > available(r) - minuse(t,r) ), alors t ne pourra pas être exécutée à l instant x. Quand x est suffisamment proche des bornes de la fenêtre de temps de t, alors on peut réduire la fenêtre de la tâche. Ainsi, par exemple : level(r)[x] requires(t,r, x) > available(r) minuse(t,r) (x t) < span(t) } t := x +1 fenêtre de temps size(t,r)= use(t,r) d(t) t1 : [0,3] 9 = 3 3 t2 : [0,5] 6 = 2 3 t3 : [3,6] 6 = 2 3 ou 3 2 t4 : [4,7] 6 = 3 2 t5 : [4,6] 2 = 1 2 t6 : [0,10] 6 = 2 3 ou L'histogramme level(r) t1 t2 t3 t5 t Une solution L'histogramme comporte 2 pics pendant lesquels t6 ne peut être exécuté: les intervalles [2,3[ et [5,6[. t6 ne tient pas avant le pic [2,3[ t6 ne tient pas entre les pics [2,3[ et [5,6[ Donc, par propagation, on peut repousser t6 après le pic [5,6[ t1 t2 t3 t5 t4 Figure 6-4: utilisation de l'histogramme de ressource pour réduire les fenêtres de temps Cette règle de propagation peut encore être affinée. On propose dans [CL96b] un algorithme linéaire en la taille de la fenêtre pour faire toute la propagation d'histogramme associée à cette fenêtre. 6.3 Stratégies d énumération On décrit maintenant l'utilisation de ces règles de propagation à l'intérieur d'un algorithme de recherche arborescente. La grande différence par rapport au cas disjonctif est qu on ne peut plus dire que pour deux 7
111 tâches partageant la même ressource, l une des deux commence après que l autre a finie. Il faut donc trouver d autres décisions pour structurer l arbre de recherche Enumération On peut envisager plusieurs stratégies d'énumération pour le problème cumulatif : 1. Comme pour tous les problèmes d'ordonnancement, les stratégies standard de PLC par énumération des dates possibles de début des tâches sont à proscrire, à cause de leur sensibilité à la discrétisation du temps. Une variante basée sur le découpage dichotomique des domaines semble cependant fournir parfois de bons résultats pour des problèmes où les contraintes de précédences dominent les contraintes de ressource [BB 96] 2. Une autre serait de raisonner sur l'ordre relatif des tâches. On pourrait ainsi raisonner sur l'ordre des dates de début (resp. l'ordre de dates de fin). Un tel schéma est incomplet car la donnée des deux ordres ne caractérise pas complètement une solution. De plus, jusqu'à présent, la propagation de telles décisions s'est avérée trop faible pour fournir des arbres de recherche de taille suffisamment petite. 3. Une troisième approche consiste à construire le PERT (ordonnancer les tâches au plus tôt, en relaxant les contraintes de ressource), et d'analyser les conflits (dépassements de capacité) les uns après les autres. Ainsi, pour réparer une telle solution en éliminant un conflit de ressource, il suffit de retarder une partie des tâches contribuant à ce pic de consommation. Cette méthode est celle utilisée par [De 92] et a l'avantage de ne construire aucun arbre de recherche quand le PERT fournit une solution admissible. 4. Une dernière approche (similaire dans l'esprit, à la précédente), consiste à construire l'ordonnancement de manière chronologique, en choisissant pour chaque tâche, soit de la faire commencer à un moment donné, soit de la retarder à plus tard. C'est cette approche que nous allons décrire. L'algorithme place ainsi les tâches les unes après les autres, à partir du début du plan (au temps 0). A chaque étape de l'algorithme, on distingue l'ensemble S, composé des tâches qui ont été ordonnancées de l'ensemble U, composé de celles qui restent à ordonnancer. Soit s la date de début au plus tôt minimale parmi les tâches de U, on distingue le sous ensemble E des tâches t dans U qui peuvent commencer au temps s (i.e. t = s). On choisit un sous-ensemble E 1 de E et l'on fait commencer toutes les tâches de E 1 à s et l'on retarde toutes celles de E-E 1. Plutôt que d'énumérer tous les sous ensembles E 1 [MM 94] on préfère procéder à une énumération implicite en considérant successivement chacune des tâches de E auxquelles on associe l'alternative binaire de commencer à s ou d'être retardées. Ceci permet de trier par ordre d'intérêt décroissant les tâches de E, et on examine d'abord les tâches les plus urgentes (celle dont la borne droite de la fenêtre de temps est minimale) ainsi que celles dont le niveau de ressource maxuse(t,r) est maximal (car le fait de les ordonnancer aura plus d'impact par propagation sur les histogrammes des ressources). Dans le cas de l'ordonnancement cumulatif, on considère autant de branches à chaque point de choix qu'il y a de niveaux de consommation possibles pour la tâche concernée. Dans le cas d'ordonnancement à plusieurs ressources, deux stratégies sont possibles, soit en cherchant à ordonnancer les ressources les unes après les autres, soit en proposant une seule procédure chronologique globale de construction Apprentissage des échecs La procédure de résolution construisant la solution de gauche à droite, ne peut donc pas se concentrer d abord sur les parties difficiles du plan (les goulots d étranglement). Ainsi, si la partie difficile de l ordonnancement est à la fin, l algorithme examinera d abord des décisions de faible importance, avant d arriver au coeur du problème. Celui-ci sera donc susceptible de résoudre de nombreuses fois des sous- 8
112 problèmes difficiles au cours de la recherche. Pour corriger cette myopie de l'arbre de recherche, on propose d'enrichir le parcours de l arbre d'une certaine mémoire des échecs passés. Ceci revient à faire de l'apprentissage des échecs : on stockera, à chaque échec, l ensemble des tâches qui restaient à faire entre le temps courant s et la fin. On appelle généralement un tel ensemble no-good set (on le notera nogood(s) ). Si l on revient plus tard dans la recherche à un problème similaire où il faille encore placer un sur-ensemble de no-good(s) à un instant s' s, on évitera de parcourir un sous-arbre et l on répondra directement que le problème associé à la branche est sans solution. Pour que ce raisonnement soit valable, encore faut-il considérer la frontière de l ordonnancement partiel construit dans ces deux branches (ce que les tâches déjà ordonnancées ont consommé des ressources après le temps s). On choisit de stocker de tels ensembles no-good que lorsqu aucune tâche n est en cours au temps s. Ceci permet de limiter la taille mémoire requise par de tels algorithmes, contrairement à l'algorithme de [DH 95] qui utilise des matrices de vecteurs de bits (représentant des ensembles de manière très compacte) allant jusqu'à 24Mo Règles de dominance Enfin, nous décrivons une technique générale pour réduire encore le nombre de solutions générées, l utilisation de règles de dominance. L idée est la suivante : parmi l ensemble des solutions possibles, beaucoup sont, d un certain point de vue, équivalentes. Par exemple, si deux tâches sont exactement identiques, toutes les solutions seront produites en double (on peut toujours intervertir les deux tâches); de même, si, dans une solution, une tâche est suivie d un temps mort, on a parfois la liberté de décaler cette tâche vers la droite dans le diagramme (i.e. la retarder) sans changer quoi que ce soit d'autre au diagramme, ce qui augmente encore le nombre de solutions similaires. L idée est de se concentrer sur un sous-ensemble représentatif des solutions, en définissant une notion de forme normale d une solution. De telles règles de dominance peuvent être utilisées comme des règles de coupes. Nous en avons identifié trois qui semblent importantes : 1. On ne considère que des solutions justifiées à gauche (ou ordonnancées au plus tôt ). Ainsi, dans chaque solution générée, on veut qu'il soit impossible de commencer une tâche plus tôt que prévu, sans que ceci se fasse au prix d'un changement pour d'autres tâches. 2. Les tâches équivalentes sont ordonnées arbitrairement (on oblige une des deux à commencer avant l'autre, et en cas de date de début identique, à avoir un niveau de consommation supérieur ou égal à celui de l'autre). 3. Enfin, lorsqu'une tâche t a un niveau de consommation tel qu aucune autre ne puisse avoir lieu en même temps qu elle et quand un ordonnancement partiel a été construit de sorte qu'à l'instant courant s, t est disponible et aucune autre tâche n'est en cours, alors on peut imposer que t commence à l'instant s (il s'agit de la règle d'incompatibilité simple de [De 92]) 6.4 Expériences et résultats Problèmes cumulatifs à une ressource Nous présentons tout d'abord une série d'expériences autour du problème de chargement de bateau [Ros 85] résolu par la contrainte cumulative de Chip [BC 94]. Il s'agit d'un problème cumulatif à 34 tâches, à faire avec une équipe de 8 personnes. Le problème tel qu'il est décrit n'est pas très difficile car il est très contraint par les relations de précédences qui limitent grandement les fenêtres de temps. Il est particulièrement intéressant d étudier les variations de difficulté du problème en fonction du nombre de contraintes de précédences. De même que si elles sont très nombreuses (comme dans le problème original), s il y a peu de contraintes de précédences, le problème est proche d un problème de placement 9
113 de surfaces et est relativement facile. Entre ces deux situations, on observe un pic de difficulté très accentué et le problème devient beaucoup plus difficile (on a en quelque sorte affaire à un phénomène de transition de phase ). Le tableau suivant compare un algorithme de gauche à droite simple, avec peu de propagation, sans apprentissage des échecs ni règles de dominance avec l algorithme complet pour des problèmes de plus en plus difficiles. Le problème original est dénoté P1, et les variations P2 à P6 sont des problèmes comportant de moins en moins de contraintes de précédences. On donne les temps de résolution et la taille de l'arbre de recherche pour trouver la solution optimale (trouver cette solution est en effet beaucoup plus difficile que de prouver l'optimalité; par exemple, l'optimalité du problème original est prouvée par simple propagation par l'algorithme complexe). Problème P1 P2 P3 P4 P6 algorithme simple 0.03 s. 65 bk s. 183 bk. 15 s bk 47 s bk. 162 s bk. algorithme complexe 0.2 s. 16 bk s. 35 bk. 8 s bk. 5 s bk. 13 s bk. Tableau 6-1: Deux algorithmes de P.P.C. sur des variations du problème de chargement de bateau Problèmes cumulatifs à plusieurs ressources On s'intéresse maintenant à la résolution de problèmes cumulatifs à plusieurs ressources. Si ces problèmes semblent très pertinents du point de vue des applications industrielles, ils sont malheureusement encore trop peu étudiés et aucun benchmark n'est disponible pour permettre une comparaison. Nous avons ainsi généré quelques instances correspondant à l'ordonnancement de tâches sur un chantier de construction. Ces problèmes comportent 30 à 37 tâches ainsi que trois ressources dont une est disjonctive (la grue) et deux sont cumulatives (deux équipes d'ouvriers). Nous comparons ci dessous trois approches: la première (algorithme complet) est notre algorithme avec tous ses raffinements, la seconde (résolution hiérarchique) essaye de tirer parti du fait qu une des 3 ressources est disjonctive en l ordonnant d abord, la troisième (propagation faible) utilise une schéma de propagation beaucoup plus faible sur les intervalles de tâches. Problème Pb1: sol. opt. Preuve opt. Pb2: sol. opt. Preuve opt. Pb3: sol. opt. Preuve opt. algorithme complet 0.14 s. 14 b s. 0 b s. 51 b. 17 s kb. 4 s. 1.5 kb. 0.2 s. 91 b. résolution hiérarchique 50 min. 3 Mb s. 0 b. 10 s b. 280 s. 120 kb. 180 s. 125 kb. 160 s. 108 kb. propagation faible 0.12 s. 17 b s. 0 b. 0.5 s. 344 b. 160 s. 82 kb. 56 s. 34 kb. 12 s. 8 kb. Tableau 6-2: problèmes cumulatifs multi-ressources 10
114 Ces expériences montrent d'une part que toute la propagation est nécessaire et, d'autre part, que la résolution hiérarchique n'est pas une bonne idée. Ce dernier résultats nous a un peu surpris. Il semble indiquer que la décomposition en sous-problèmes suivant les ressoures est une mauvaise idée. Nous avons aussi adapté l algorithme pour qu il puisse prendre en compte des niveaux de consommation variables (cas semi-préemptif). Le schéma de branchement décrit peut être adapté à ce cas, et il se trouve que les problèmes deviennent alors un peu plus faciles à résoudre. La flexibilité donnée par la partie molle des tâches semi-préemptives fait qu il est plus facile de trouver une solution lorsqu il en existe une. Nous pouvons conclure en remarquant que, sur l'ensemble des deux jeux d'expériences (cumulatif monoressource pour le chargement de bateau et cumulatif multi-ressource pour la planification de chantier), chacune des techniques exposées (règles de propagation redondantes, schéma de branchement, règles de dominance et apprentissage des échecs) s'est avérée indispensable à la résolution d'au moins un des problèmes. Les problèmes d'ordonnancement cumulatif, même de petite taille, sont donc difficiles à résoudre Le RCPSP (Resource Constrained Project Scheduling Problem) La dernière série d'expériences que nous décrivons s'intéresse à la résolution d'instances du RCPSP. Le but de ces expériences est de fournir une évaluation de l'approche qui a été développée en la testant sur des problèmes sur lesquels on dispose de points de référence. L'algorithme qui a été présenté n'a pas été spécialement optimisé pour le RCPSP. La première série d'expériences a été menée sur la série des 110 problèmes générés par Patterson [Pa 84], qui comportent 3 ressources, et 7 à 50 tâches. Les meilleures performances sont celles de Demeulemeester [De 92] qui les résout tous en une moyenne de 0,2 s. par problème (les temps de résolution ont encore été améliorés dans [DH 95]). L'algorithme décrit dans ce chapitre résout ces problèmes en une moyenne de 3,5 s. (Pentium 90) et 1000 backtracks par problème (34 sont résolus en moins de 10 backtracks). Le nombre de backtracks indique que ces problèmes sont faciles et que l'algorithme proposé sait bien les résoudre. Comme ces problèmes sont faciles, l'approche lourde par contraintes (prise en compte du cas général et règles de propagation complexes) est pénalisée en temps par rapport à des algorithmes spécialisés. Nous avons donc mené une deuxième série d'expériences sur des problèmes réputés difficiles, la série des 480 problèmes KSD30, proposés dans [KS 95]. Ces problèmes se répartissent en 48 séries de 10 problèmes, chacun d'eux comportant 30 tâches sur 4 ressources. Les séries sont très irrégulières, certaines très faciles, d'autres plus difficiles (la plus difficiles est la série numéro 13). Actuellement, à notre connaissance, seuls Demeulemeester et Herroelen arrivent à résoudre (trouver une solution optimale et prouver son optimalité) l'ensemble des problèmes [DH 95]. On relate maintenant des expériences sur les 12 premières séries de 10 problèmes. Si l'on alloue une seconde de calcul par problème, on résout complètement 80 des 120 premiers problèmes. En allouant, 1 minute par problème, on résout 91 des 120 problèmes, on trouve la solution optimale sans la prouver sur 17 autres, et sur les 12 restant, on se trouve à une distance totale de 29 unités, ce qui fait une distance moyenne de 3.7% sur ces 12 problèmes. Les expériences sur la série numéro 13 sont plus complexes. En effet, comme l'indiquent Baptiste et Le Pape [BL 97], les problèmes les plus difficiles parmi ces séries ont la particularité d'être très disjonctifs, c'est à dire que de nombreuses paires de tâches ne peuvent pas s'exécuter en parallèle parce que la consommation cumulée des deux tâches dépasse le seuil disponible. Si de telles disjonctions sont nombreuses, le problème perd son aspect géométrique de placement optimal de rectangles (la solution optimale comporte une surface non utilisée importante dans son diagramme). Le raisonnement d'edge- 11
115 finding, basé sur la prise en compte des surfaces, perd alors une grande partie de son efficacité. Pour bien traiter de telles situations, il faut raisonner sur ces disjonctions de manière effective. La seule règle décrite jusqu'à présent consiste à détecter les paires de tâches disjonctives à cause de leurs demandes en ressource et à propager la disjonction en look-ahead (quand un des deux ordres entre deux tâches devient impossible, on impose l'autre). Comme l'expression des seules disjonctions était très inefficace pour résoudre le problème du job-shop, on va, ici aussi chercher à faire un raisonnement global sur les disjonctions. On définit une clique comme un ensemble de tâches qui deux à deux sont toutes disjonctives (soit pour une raison de demande de ressource, soit par l'existence d'une précédence explicite). Ces cliques sont donc liées par exactement les mêmes contraintes que si les tâches partageaient une ressource disjonctive. On peut donc créer une contrainte redondante de ressource disjonctive pour chaque clique. Comme ce mécanisme de propagation est relativement lourd, on se limite à la création d'une contrainte redondante disjonctive (liée à une clique) par ressource cumulative (on suit la procédure proposée dans [BL 97]). Le tableau suivant donne les résultats détaillés pour la série numéro 13, avec et sans ces contraintes redondantes. Algorithme standard avec les contraintes disjonctives redondantes 1 sec. 1 min. 3 mins. 1 sec. 1 min. 3 mins. ksd ksd ksd ksd ksd ksd ksd ksd ksd ksd Total % % % % % % Tableau 6-3: Distance à l'optimum de la solution trouvée par un algorithme de contraintes (avec ou sans contraintes redondantes) sur des problèmes difficiles de RCPSP Ce tableau confirme le résultat de [BL 97], à savoir que les contraintes redondantes disjonctives sont indispendables à la résolution de ces problèmes. De plus, on voit que les résultats obtenus par l'algorithme (à 2.79% de l'optimum sur la série la plus difficile, après 3 minutes de calcul sur un PC Pentium à 200Mhz.) sont tout à fait honorables. Ces résultat valident ainsi l'approche décrite pour le problème général de l'ordonnancement cumulatif. 12
116 6.5 Autres techniques Pour finir, on décrit deux techniques reliées au problème d'ordonnancement cumulatif. Les deux premières sont des techniques de propagation, qui en pratique, n'ont pas encore donné de bons résultats Raisonnement énergétique Le raisonnement énergétique, proposé par Lopez [Lo 91] initialement pour le cas disjonctif (cf ) est basé sur l idée suivante : on considère une certaine famille d intervalles de temps [a,b], et pour chaque tâche t, on évalue la quantité minimale de t dont on peut prévoir qu'elle sera nécessairement faite entre a et b (l'énergie de t entre a et b). Une contrainte redondante affirme alors que la somme des énergies de toutes les tâches t sur la ressource r est inférieure à (b-a) available(r). Cette technique est très générale : = si l'on prend b=a+1, on retrouve la contrainte d intégrité sur l histogramme, = si l'on prend pour [a,b] une fenêtre de temps correspondant à un intervalle de tâches, l inéquation obtenue est plus forte que la contrainte d intégrité des intervalles de tâches (puisqu on prend aussi en compte la contribution de tâches qui ne sont pas totalement comprises entre a et b), = si l'on prend a=s et b correspondant à la borne droite de la fenêtre de temps d'une tâche, on obtient une contrainte d'intégrité un peu plus forte que celle sur les semi-intervalles de tâches. Il est cependant déraisonnable de vouloir appliquer le raisonnement énergétique à tous les intervalles temporels et ceux qui correspondent aux intervalles de tâches paraissent un sous-ensemble intéressant. On peut ainsi intégrer la contribution énergétique dans les règles de propagation pour les renforcer. On souhaiterait alors pouvoir faire la mise à jour des contributions énergétiques dans l algorithme de maintenance des intervalles de tâches. Malheureusement, la complexité de la procédure de mise à jour d'un intervalle passe de O(n 2 ) à O(n 3 ), ce qui pénalise trop le temps d exécution pour en valoir la peine Shaving Le shaving est une technique de réduction des domaines [MS 96], qui a déjà été présentée dans le cas de l'ordonnancement disjonctif (voir 3.3.3), où elle apporte des progrès spectaculaires sur certains problèmes difficiles. L'idée est d'essayer d'affecter des dates d'exécution aux tâches, de propager, et, en cas d'échec, de retirer la valeur en question de la fenêtre de temps de la tâche. Il s'agit ainsi d'un mécanisme très général de cohérence des domaines. Bien réglé, il permet de résoudre le célèbre problème MT10 en 7 backtracks au lieu de 1500 (même si ce chiffre 7 n'est pas très honnête puisqu'on pourrait aussi comptabiliser des retours arrières internes à la procédure de shaving), et de résoudre le problème LA21 en 4 heures au lieu de 36. Dans le cas de l'ordonnancement cumulatif, nos expériences ont été peu concluantes, les réductions en nombre de backtracks sont bien moindres que celles obtenues pour l'ordonnancement disjonctif. La situation est encore pire dans quand les niveaux de consommations ne sont pas fixés. Dans ce cas, le shaving n apporte presqu aucune information. En fait, il semble que cette technique de consistance soit efficace dans des cas où l'on a un algorithme de propagation très fort, qui contraint beaucoup le système à chaque décision. Il semblerait que cette propriété se perde quand on passe du job-shop au RCPSP, puis à l'ordonnancement cumulatif général. 6.6 Bilan Alors que pour le problème du job-shop, on sait résoudre facilement des problèmes à plusieurs centaines de tâches, ce n est pas le cas pour l ordonnancement cumulatif. La limite des problèmes que l'on sait 13
117 résoudre de manière exacte réside autour de trente tâches sur quelques (3-5) ressources. Au delà, on sait résoudre seulement certaines classes de problèmes. Une grande différence entre l'ordonnancement disjonctif et cumulatif est la variabilité des problèmes. On peut être confronté à des problèmes forts différents suivant l'importance dans les données, des contraintes de précédence et suivant les niveaux de consommation de ressource qui rendent le problème plus proche d'un problème d'ordonnancement (plus facile) ou d'un problème de placement géométrique (plus difficile). Suivant le type de problème, les techniques à utiliser (contraintes globales, relaxation) ne sont pas les mêmes. On a présenté dans ce chapitre un cadre général de résolution des problèmes cumulatifs que l'on a appliqué à des problèmes très différents. La qualité des résultats obtenus indique que cet algorithme est robuste. Pour la résolution de problèmes plus gros, la définition de cadres d'optimisation locale ainsi que d'heuristiques autres que la construction chronologique est un domaine de recherches complètement ouvert. Une direction de recherche pourrait être d'expérimenter des techniques d'optimisation locale prenant en compte de la propagation de contraintes (comme la méthode shuffle utilisée en ordonnancement disjonctif). 14
118 15
119 7. Chapitre 7. Problèmes de tournées Nous étudierons dans ce dernier chapitre une généralisation du problème du voyageur de commerce au cas de plusieurs voyageurs : il s'agit alors de proposer un ensemble de tournées qui couvre l'ensemble des villes à visiter. Contrairement au TSP, le problème des tournées a de nombreuses applications industrielles et l'optimisation du parcours peut avoir une grande importance lors de la planification de ces tournées. Un algorithme original (O.L.I.) a été proposé dans [CL 97d] pour résoudre des problèmes de tournées de grande taille. Nous le présenterons ici en le comparant aux approches classiques de Recherche Opérationnelle et nous montrerons comment il permet d'obtenir des résultats compétitifs avec l'état de l'art sur des problèmes de tournées avec fenêtres de temps. 7.1 Tournées de véhicules Typologie, applications Le problème des tournées (VRP pour Vehicle Routing Problem) est une généralisation du TSP où l on ne cherche plus un cycle, mais un ensemble de cycles qui couvre tous les sommets du graphe. Ces cycles ne sont généralement pas quelconques : = on particularise un sommet (appelé le dépôt), d'où commencent et où aboutissent toutes les routes, = on borne les longueurs de chacune de routes par une constante M, = on borne le nombre de routes par un entier K. On cherche alors un ensemble de routes vérifiant ces conditions, et dont la longueurs totale (la somme des longueurs des tours), soit minimale. On peut aussi chercher à minimiser d'abord le nombre de routes, puis pour ce nombre minimal, minimiser la longueur totale. Ces problèmes de tournées sont à la base de nombreux problèmes de logistique, où une route correspond au travail d un homme ou d une équipe. Cette échelle humaine fait que la plupart du temps, les routes sont de taille limitée (5 à 20 noeuds), alors que le problème complet peut être très grand (10000 nœuds). Pour modéliser finement ces applications réelles, on est souvent amené à introduire des contraintes supplémentaires. Par exemple, : = on peut considérer plusieurs dépôts dont peuvent partir les routes, = on peut considérer la distance comme un temps de trajet entre les nœuds et associer une fenêtre de temps contraignant la date de visite des nœuds (on parle alors de Vehicle Routing Problem with Time Windows, VRPTW), = on peut associer à chaque nœud une quantité de marchandise, à livrer ou à enlever (pickup and delivery problem), et imposer qu'à chaque instant le stock de marchandises enlevées le long de la route ne dépasse pas la capacité du véhicule. On peut même considérer l'agencement physique des marchandises dans la remorque, et imposer que celles à livrer soient accessibles (qu'elles ne soient pas au fond du camion), etc. Enfin, dès que le problème comporte un aspect temporel (c'est le cas du VRPTW), les objectifs d'optimisation peuvent être multiples. On peut vouloir minimiser la distance géographique, ou le temps total de transport, ou encore la somme des temps d'attente (qui se produisent lorsqu'une tournée arrive trop tôt en un nœud), etc. 1
120 7.1.2 Relation entre le TSP et le VRP Le problème du VRP peut être découpé en deux sous-problèmes : un problème de partitionnement des nœuds du graphe en sous-ensembles (associés à chacune des routes) et, pour chacun de ces ensembles, un problème de séquencement (i.e. un problème de voyageur de commerce). On peut se baser sur cette décomposition du problème pour proposer une procédure de résolution en deux phases qui choisisse d'abord de regrouper les nœuds par routes, puis de résoudre, pour chaque route, un TSP. De nombreuses heuristiques sont basées sur cette méthode en deux passes qui a l avantage d être très rapide (le partitionnement peut être fait suivant des critères simples comme l angle polaire vis à vis du dépôt et les petits TSP peuvent être résolus de manière optimale par programmation dynamique ou par programmation par contraintes) Figure 7-1: réduction du VRP à un TSP On peut aussi réduire le problème des tournées à un problème de voyageur de commerce si l on accepte d ignorer les contraintes de capacité: pour trouver une solution utilisant au plus K routes, on éclate le noeud du dépôt en K copies et l on assigne une distance constante D entre ces copies du dépôt. La figure 7-1 montre comment on peut obtenir une solution au VRP à partir d'un cycle Hamiltonien sur ce problème modifié. On coupe le cycle à chaque copie du dépôt; on obtient alors un ensemble d'au plus K chaînes, que l'on peut considérer chacune comme une route. Si l'on choisit une valeur positive suffisamment grande pour D, la solution de coût optimal à ce problème de voyageur de commerce ne visitera jamais deux copies du dépôt de manière consécutive, et on obtiendra la solution du problème de tournées de distance totale minimale utilisant exactement K routes Modélisation linéaire Pour formaliser le problème, on note K le nombre de camions disponibles (nombre maximal de routes), 0 le dépôt et {1,.., n} l ensemble des autres noeuds du graphe. On note q i la quantité de marchandise associée au noeud i et, pour 1 k K, on note Q k la capacité du camion k. On cherche une solution en K routes, chacune de longueur maximale M. De multiples modélisations linéaires ont été proposées pour le VRP (pour un exposé plus complet, voir [Bo 83], [La 92] ou [La 97]). Nous décrivons ici l'une d'entre elles (celle dite des trois index). On introduit des variables x ijk à valeur dans {0,1} et valant 1 si et seulement si le camion k passe par l'arête ij ainsi que des variables entières y jk dénotant le nombre d'arêtes de la route k arrivant en j. Le problème revient alors à trouver : K min x ijk.d(i, j) k=1 i j sous les contraintes 2
121 y jk = y ik k x i ijk { = m si i = 0 1 sinon q i.y i ik Q k x ijk.d(i, j) i j M S V t.q. S 2, x ijk S 1 i, j S On reconnaît dans cette formulation les contraintes de capacité, les contraintes sur la longueur maximale d'une route ainsi que les contraintes d'élimination de sous-tours, sur chacun des problèmes de voyageur de commerce associés à une route. Cette formulation linéaire (ou d'autres) peuvent être utilisées par une procédure de résolution utilisant les méthodes de programmation linéaire en nombres entiers : branch and bound, si l'on parcourt un arbre de recherche en utilisant la borne de la relaxation linéaire à chaque nœud, branch and cut si l'on choisit, en plus, de générer de nouveaux plans de coupes à chaque nœud de l'arbre de recherche. L'apport du branch and cut (technique plus lourde, car on passe beaucoup plus de temps à chaque nœud) semble moins déterminant que pour le TSP. Il permet néanmoins de résoudre des problème d'une centaine de nœuds [Au 95]. De nombreuses approches effectuant des recherches arborescentes ont été proposées, utilisant d'autres bornes que la relaxation linéaire. Par exemple, un certain nombre de formulations utilisent la décomposition du problème en un problème de couverture et de partitionnement pour poser des points de choix dans un des deux modèles (par exemple, en utilisant un encadrement des problèmes individuels de TSP, développer un arbre de recherche pour le problème de couverture). Ces approches sont toutes confrontées au même problème d'explosion combinatoire que le branch and cut. Certaines sont utilisées comme heuristiques, pour faire des recherches incomplètes (en ne parcourant pas tout l'arbre). 7.2 Algorithmes de résolution Programmation dynamique On présente, comme pour le TSP, un algorithme simple de résolution en programmation dynamique. Pour chaque ensemble de noeuds T, on calcule par programmation dynamique tsp(t), comme étant la valeur du plus petit cycle Hamiltonien pour T. Puis, pour tout k, si la quantité de marchandise associée à T est supérieure à Q k ou si tsp(t) > M on pose TSP(T,k) = + et sinon TSP(T,k) = tsp(t). Enfin, la fonction f permet de calculer la valeur optimale du problème. f (1,T) = TSP(T,1) f (k,t) = min S T ( f (k 1, T S) +TSP(S,k) ) Ainsi, F = f(k, {1,.., n}) est la longueur minimale d un plan de tournées (le cas F = + correspond à un problème insatisfiable). Comme pour le cas du TSP, cette formulation présente l'avantage de permettre la prise en compte de nombreuses contraintes annexes, mais on ne peut l'appliquer qu'à des petits problèmes à cause de la taille mémoire requise. 3
122 En revanche, la formulation en programmation dynamique du TSP peut elle s'avérer très utile pour la résolution du VRP, pour obtenir des bornes dans un algorithme de recherche (branch and bound). On peut ainsi résoudre de manière exacte des problèmes jusqu'à nœuds et une dizaine de camions Heuristiques Puisque les approches exactes ne peuvent traiter que des problèmes beaucoup plus petits que ceux envisagés dans les applications réelles, nous allons nous concentrer sur les approches heuristiques. Comme pour le TSP, les heuristiques sont très nombreuses. On trouve ainsi des procédures géométriques spécialisées pour les problèmes à distance Euclidienne, etc. La plupart de procédures de résolution fonctionnent en deux phases, cherchant d'abord à obtenir une bonne solution, puis cherchant à l'améliorer. Une heuristique simple et qui donne de bons résultats est celle des économies (savings) due à Clarke et Wright [CW 64]. L idée est la suivante: on commence par créer n routes élémentaires 0-i-0 pour tous les sommets i. Puis on procède n-k fois à un raccordement de deux routes, l une à la suite de l autre. A chaque raccordement, la distance totale diminue. On choisira à chaque étape le raccordement qui diminue le plus la longueur totale (il s agit donc d un algorithme glouton), tout en respectant la contrainte de capacité. Cet algorithme donne de très bons résultats sur le problème pur (si les contraintes de capacité ne sont pas trop fortes), mais il s'adapte relativement mal aux contraintes supplémentaires. Une autre famille d'heuristiques construit l'ensemble des routes par une suite d'insertions de coût minimum. On part de K routes vides, l'algorithme examine les sommets un à un, et choisit de les insérer entre deux sommets dans une route, de sorte que cette insertion fasse augmenter le moins possible la longueur de la route. On affine généralement un peu cette procédure en favorisant les insertions dans des routes vides (par exemple en comptabilisant un coût de d(0,i) au lieu de 2d(0,i) ), qui sinon, sembleraient toujours moins intéressantes que des insertions dans des routes existantes, ce qui conduirait à des solutions déséquilibrées (on insérerait des sommets dans la première route jusqu à ce qu elle soit pleine, puis on commencerait une seconde route, etc. ). Ces procédures d'insertion dépendent donc de l'ordre dans lequel on envisage des sommets à insérer. De nombreuses variantes sont proposées, suivant que l'on envisage d'abord les sommets les plus loin du dépôt, ceux le plus près, au hasard, ou encore suivant un angle polaire sur la carte, etc. Ces heuristiques sont en général peu stables, et donnent des résultats de qualité médiocre. En revanche, celles-ci ont de nombreuses qualités : elles sont simples à programmer, de complexité faible, et surtout, elles peuvent aisément prendre en compte des contraintes additionnelles comme des fenêtres de temps. Ces qualités de génie logiciel expliquent leur succès (la majorité des logiciels commerciaux de tournées utilisent des procédures d'insertion). Mentionnons encore qu'une des meilleures heuristiques connues à ce jour est une procédure d'insertion généralisée, GENI [GHL 94], qui choisit d'insérer un sommet i entre deux sommets x et y d'un tour r, sans que ceux-ci ne soient nécessairement consécutifs dans r avant l'insertion. Ceci revient à faire en même temps une insertion et une réparation du tour : pour insérer i entre x et y, on est amené à casser la route en plusieurs chaînes et à les reconnecter autrement (en changeant éventuellement le sens de parcours d'une des chaînes) Optimisation locale k-opt Les deux heuristiques gloutonnes décrites ci-dessus ne remettent jamais un choix en cause (que ce soit l'insertion d'un nœuds dans une route ou la fusion de deux routes) et ont donc beaucoup à gagner à être suivies d une phase d optimisation locale pour réparer certains choix. Les méthodes générales k-opt du TSP s'appliquent aussi au problème des tournées. D'une part, on peut optimiser chacune des routes de manière individuelle, en effaçant k arêtes d'un même tour et en cherchant à recombiner autrement les 4
123 sous-chaînes. D'autre part, on peut chercher à effacer k arêtes de tours différents et à reconnecter les portions de routes. Ce dernier type de mouvements ne se contente pas de remettre en cause la succession des sommets dans un tour, il change aussi la partition des sommets en routes. parcouru en sens inverse Figure 7-2: deux mouvements k-opt pour le VRP : un mouvement 3-opt à 3 routes et un mouvement 3-opt à 2 routes avec inversion du sens de parcours d'une sous-chaîne Pour les grands problèmes que l'on envisage, la première procédure (optimisation des tours individuels) a généralement une complexité faible (car les routes individuelles sont de petite taille), mais n'apporte que des gains modérés. La procédure d'échange d'arêtes entre routes a elle, une complexité significative (le coût de 3-opt devient rapidement prohibitif pour des problèmes à partir du millier de nœuds), et offre des améliorations intéressantes. Cependant, contrairement au cas du TSP pur, où les procédures k-opt permettaient d'approcher de très près l'optimum, même pour les valeurs faibles de k (2 ou 3), le VRP semble requérir l'utilisation de certains mouvements compliqués (comme ceux de type 4-opt qui permettent l'échange de sommets ou de sous-chaînes) pour pouvoir sortir d'optima locaux relativement éloignés de l'optimum. On est ainsi confronté à une situation différente du TSP, puisque la faiblesse relative du cadre d'optimisation locale incite au développement d'heuristiques de bonnes qualité. Signalons enfin que d'autres cadres d'optimisation locale ont été proposés. Ceux-ci envisagent généralement des mouvements complexes dans un voisinage de taille limitée (par exemple, l'exploration d'un sous ensemble du voisinage 4-opt ou 5-opt en temps quadratique [GHL 94]). Une optimisation courante consiste aussi à appliquer un filtre géographique pour borner la recherche aux plus proches voisins : ainsi quand on voudra remplacer une arête xx par xy, on ne cherchera y que parmi les p sommets les plus proches de x (p étant un paramètre à régler en fonction du temps de calcul accepté). Enfin, le VRP est un des problèmes sur lesquels les méta-heuristiques, comme la recherche tabou ont eu de grands succès [RT 95]. On aura donc intérêt à faire visiter les voisinages par de telles marches aléatoires (on rappelle que la recherche tabou est un cadre d'algorithmes d'optimisation locale où l'on garde une mémoire des solutions déjà rencontrées, voir 1.4.2). On pourra par exemple stocker dans une liste tabou les arêtes effacées par ces transformations (stocker plus d informations comme les paires (arêtes effacées, arêtes nouvelles) conduit généralement à des marches aléatoires cycliques; stocker moins d information comme les sommets incidents aux arêtes modifiées conduit à bloquer le système qui ne trouve plus de mouvements). Enfin, on mentionnera l'approche par optimisation locale guidée [KPS 97] qui consiste à faire de l'optimisation locale sur une fonction de coût modifiée, en incorporant une mémoire des états visités (comme une liste tabou). Mentionnons encore que de nombreuses procédures d'optimisation locale proposées considèrent des mouvements qui passent la frontière entre solutions réalisables et irréalisables (en attribuant un coût fini aux violations de contraintes comme, par exemple, le dépassement d'une limite de capacité sur une route, ou le dépassement du nombre de routes). De tels mouvements permettent d'échapper plus facilement de 5
124 minima locaux, mais ont comme inconvénient de ne pas toujours laisser une solution réalisable à l'utilisateur (ces algorithmes sont donc difficilement interruptibles). 7.3 Insertion et optimisation locale incrémentale On présente dans ce paragraphe une nouvelle heuristique basée sur la coopération fine entre un algorithme d'insertion et un algorithme d'optimisation locale [CL97c]. Le principe consiste à enrichir une procédure d'insertion en faisant un peu d'optimisation locale après chaque insertion. Bien entendu, pour garder un caractère pratique à une telle procédure, il faut que la phase d'optimisation locale soit de complexité limitée. Nous verrons comment ce principe simple donne lieu à un algorithme robuste, fournissant des solutions de bonne qualité en un temps de calcul très court. L'idée de cette procédure est que, puisque l optimisation locale permet de réparer les erreurs faites par l heuristique gloutonne, il vaut mieux faire ces réparations au fur et à mesure que les défauts apparaissent dans la solution, plutôt qu'à la fin, une fois que l'on dispose d'une solution complète (ce qui correspond à l'utilisation habituelle de l'optimisation locale). Ceci a l avantage de limiter la zone d optimisation puisque l'on n'effectue des réparations que localement, à l endroit de l insertion. Il s agit donc d optimisation locale incrémentale (O.L.I). L'idée d'une telle procédure hybride se situe dans la veine de la procédure d'insertion généralisée GENI [GHL 92], qui consiste à insérer un sommet entre deux sommets quelconques d'une route (ce qui peut être simulé par une insertion simple, entre deux sommets consécutifs du tour, suivie d'une passe de 4-opt sur le tour). De plus, une procédure mêlant construction et optimisation locale a déjà été expérimentée par [Ru 95], qui applique une forme limitée d'optimisation locale toutes les k insertions. Notre approche est nouvelle dans la mesure où l'on va chercher à faire des mouvements variés et relativement compliqués autour du point d'insertion (mouvements entre plusieurs routes, optimisation exacte des tours, etc.). La clé du succès de cette heuristique sera d'arriver à introduire une grande variabilité par ces mouvements locaux, tout en gardant une complexité faible pour que l'algorithme soit apte à résoudre des grands problèmes. En conclusion, nous verrons que le schéma d'algorithme mèlant construction gloutonne et optimisation locale incrémentale est un schéma général qui peut être appliqué à d'autres problèmes d'optimisation Algorithme d'insertion L'étape d'insertion du nœud i parmi un ensemble de routes peut alors être décrite de la manière suivante. Pour chacune des routes r, on cherche l'arête xy de r telle que le coût d'insertion d(x,i)+d(i,y)-d(x,y) soit minimal, ce qui fournit le coût d'insertion de i dans r. On choisit enfin d'insérer i dans la route r dont le coût était minimal. Pour le cas du VRP sans contraintes annexes, on cherchera à insérer les sommets les plus lointain du dépôt en premier. L'algorithme d'insertion correspond à une recherche limitée à profondeur 1, soit un compromis entre une heuristique gloutonne aveugle et un arbre de recherche plus complet. De tels algorithmes effectuant une recherche en avant (look-ahead) sur un niveau de profondeur de l'arbre de recherche, ont déjà été rencontrés en ordonnancement disjonctif ( 3.4.1) et pour les emplois du temps ( 5.4.2). Comme nous l'avons mentionné en introduction, ces heuristiques d'insertion fournissent en pratique des résultats de mauvaise qualité. Une première possibilité pour les améliorer consiste à faire suivre de tels algorithmes d'une passe d'optimisation locale 2 ou 3-opt. Comme point de référence pour évaluer la procédure d'optimisation locale incrémentale que nous proposons, nous allons considérer une méthode en deux passes qui obtient une première solution par une heuristique d'insertion, puis l'améliore en appliquant des mouvements locaux de type 2 et 3-opt. On choisit (suivant la stratégie usuelle de descente) 6
125 d'appliquer de tels mouvements tant que l'on en trouve que améliorent le coût de la solution (la distance totale parcourue). On autorise aussi de petits plateaux au long de cette recherche, en admettant de faire au plus 5 mouvements 2 ou 3-opt qui conservent le coût de la solution (gain nul). Cette procédure naïve d'insertion peut être améliorée si l'on optimise r pour chacune des insertions prospectives, avant d'en évaluer le coût. Ceci permet de faire un choix (celui de la route dans laquelle on insère) un peu plus informé que dans la version simple de l'heuristique d'insertion gloutonne, et de corriger ainsi un peu la myopie inhérente à tous les algorithmes gloutons. Si l'on considère une procédure d'optimisation locale simple (on peut, par exemple, procéder à une passe de 3-opt sur le petit TSP associé à r), on obtient une première version naïve d'un algorithme mêlant insertion gloutonne et optimisation locale incrémentale (O.L.I). CW Insertion O.L.I. naïve N=200, pb # (9) 1 s (10) 0.02 s (9) 0.4 s. N=200, pb # (12) 1s (13) 0.02 s (13) 0.9 s. N=200, pb # (10) 1 s (12) 0.02 s (12) 0.1 s. Tableau 7-1: comparaison d'heuristiques sur des problèmes aléatoires de tournées Le tableau 7-1 présente trois expériences sommaires comparant l'heuristique des économies (notée CW), la procédure d'insertion gloutonne et la version la plus naïve d'optimisation locale incrémentale. On présente les résultats sur trois problèmes générés de manière aléatoire à 200 nœuds, et on indique pour chaque problème la distance totale de la solution, suivie, entre parenthèse, du nombre de routes utilisées. On peut ainsi voir d'une part, que les performances de l'heuristique d'insertion sont très médiocres, comparées à l'algorithme des économies, et d'autre part, que l'optimisation individuelle des tours par une passe de 3-opt (O.L.I. naïve) permet d'améliorer de manière significative les résultats de l'heuristique d'insertion. On va maintenant présenter la version complète de l'algorithme d'o.l.i. On renvoie le lecteur à [CL 97c] pour une présentation en détail. Un point intéressant à noter est que cet algorithme est basé sur les mêmes idées que celui présenté au pour la résolution de problèmes d'emplois du temps (application à la gestion de personnel). Il semble ainsi que la synergie entre algorithmes constructifs (par insertion) et optimisation locale incrémentale puisse être utilisée sur des problèmes différents Optimisation locale incrémentale Comme dans la première version naïve de l'heuristique O.L.I. qui vient d'être décrite, on va, à chaque étape de l'algorithme, évaluer pour chaque route, la meilleure place d'insertion possible (entre deux sommets consécutifs de la route), puis après avoir effectué cette insertion, on cherchera à optimiser localement la solution, avant d'évaluer le coût de l'insertion. Les mouvements qui seront considérés seront des mouvements de type 2-opt ou 3-opt. Après l'insertion du sommet i dans la route r, on commencera par chercher un mouvement 2-opt séparant la route r juste avant ou juste après i (on coupe donc l'arête Pred[i],i ou l'arête i,next[i]) pour recombiner le début de r avec la fin d'une route r' et la fin de r avec le début de r'. Si aucun tel mouvement n'est trouvé, alors on cherche s'il existe une chaîne y y' sur une autre route r' qu'il serait avantageux de transférer juste après i. L'étape d'optimisation locale incrémentale peut ainsi être décrite de la manière suivante : 7
126 incremental_opt(i:dom,r:route) for r' in (Route but r) for all pairs of edges (x,y) and (y,z) on r stop the iteration when either xchange(r,i,next[i],r',x,y)) is successful or xchange(r,pred[i],i,r',x,y)) is successful or (d[x,y] > d[i,y] and d[y',z] > d[next[i],y'] and transfer(y,y',r',i,r) is successful) Les deux paragraphes qui suivent décrivent les procédures xchange et transfer utilisées par cette procédure Mouvement 2-opt entre deux routes Le premier mouvement (figure 7-3) échange deux arêtes sur deux routes et raccorde le début de l'une avec la fin de l'autre. On cherche de tels mouvements qui fassent diminuer la longueur totale. Quand on vient d'insérer le sommet i ou j dans la route r, on chercher une autre route r' et une arête xy sur r' telle que l'échange des arêtes ij et xy par iy et xj diminue la longueur totale. Quand on trouve un tel mouvement, on réoptimise individuellement les deux routes r et r' (par du 3-opt sur chacun des deux petits TSP), puis on cherche d'éventuels nouveaux échanges d'arêtes sur la fin de r (à partir de y) et sur la fin de r' (à partir de j). Enfin, on regarde si les modifications sur r' rendent possible des transferts de sommets vers r'. r r i x y j Figure 7-3: échange d'arêtes entre r et r' les liens en pointillés sont remplacés par les liens en gras : on recolle le début de r: dépôt i avec la fin de r': y dépôt et le début de r': dépôt x avec la fin de r: j dépôt) Mouvement de transfert d'une sous-chaîne d'une route à une autre Le deuxième mouvement (figure 7-4) que l'on considère coupe trois arêtes sur deux routes (ij sur r, xy et y'z sur r'), supprime une sous chaîne (y y') d'une des deux routes pour l'insérer entre deux sommets consécutifs (i et j) de l'autre. Quand la sous-chaîne est réduite à un sommet, ce mouvement revient à un transfert de sommet. Ici encore, on ne cherche de tels mouvements que lorsqu'on vient d'insérer le nœud i dans la route r. Si l'on trouve un tel mouvement qui diminue la longueur totale de l'ensemble des tournées, on cherche à réoptimiser individuellement r et r', puis on regarde si r', dont la longueur totale a diminué, peut éventuellement accepter des transferts de sommets. 8
127 r y i x r z y' j Figure 7-4: transfert de sous-chaîne (y y' quitte r' pour être insérée dans r, après i) Dans ces deux mouvements, on a envisagé la possibilité d'insérer de nouveaux sommets dans une route. Ceci correspond à une utilisation limitée de la procédure transfer, quand la chaîne à transférer est réduite à un seul sommet. Enfin, soulignons que dès qu'une route a été modifiée par un mouvement d'optimisation locale, on cherche à réoptimiser cette route par une passe de 3-opt Insertions irréalisables Une fois que l'on a calculé chacun de ces coûts, l'insertion de i est possible dans certaines routes, impossible dans d'autres, à cause des contraintes de capacité (ces routes sont déjà pleines ). On choisit alors de faire l'insertion dans celles des routes qui offre le coût d'insertion le plus faible, puis on fait une passe d'optimisation locale incrémentale, décrite au paragraphe précédent. En fait, nous proposons d'enrichir encore un peu cette procédure pour mieux tirer parti de ces insertions irréalisables. Pour toutes ces routes qui ne peuvent accepter l'insertion du nouveau sommet i, on évalue le coût d'insertion minimum de i entre deux sommets consécutifs de la route. Cette quantité nous fournit une estimation de ce que serait le coût d'insertion si celle-ci était réalisable. Dans les cas où un telle insertion irréalisable semble particulièrement prometteuse (le coût de cette insertion, semble bien inférieur au coût de la meilleure insertion réalisable), on envisage d'autres mouvements d'optimisation locale pour essayer de permettre cette insertion. Le premier mouvement que l'on envisage consiste à appliquer la procédure transfer à l'envers : pour pouvoir insérer i dans r, on cherche à déplacer une chaîne y y' de r vers r' (en espérant libérer ainsi de la place dans la route r et rendre ainsi réalisable l'insertion de i). Le second mouvement est plus original et est lié à la notion de chaînes augmentantes (voir le chapitre 2) ainsi qu'aux procédures de transfert cycliques [TP 93]. On cherche à insérer le sommet i dans la route r qui est pleine. Pour rendre cette insertion possible, on cherche un autre sommet j dans r tel que si le départ de j de r libère assez de place pour y permettre l'insertion de i. On peut ainsi chercher tous les sommets j qui ont cette propriété. Puis on peut répéter cette analyse, en cherchant pour chacun des sommet j, un sommet k appartenant à une autre route et pouvant laisser sa place à j. En répétant cette analyse, on construit une séquence de sommets i,j,k,... où i est inséré dans la route de j, j dans celle de k, etc. Cette séquence se termine quand on trouve un dernier sommet qui peut être inséré librement dans une route, sans en chasser aucun des sommets qui s'y trouve. Pour rester dans le cadre de mouvements simples, on impose aussi que les sommets de cette séquence appartiennent tous à des routes différentes. Pour chercher une telle séquence, on peut développer un arbre de recherche, ayant i pour racine, et où les fils d'un nœud x sont les nœuds y qui peuvent laisser leur place à x, et où les feuilles correspondent à des succès sont les sommets qui peuvent être librement insérés dans une autre route. Quand on a trouvé une feuille, on peut remonter le long de la branche jusqu'à la racine de l'arbre en procédant à une suite de transferts de nœuds d'une route à une autre, pour rendre réalisable l'insertion de i. Notons que si l'on 9
128 représente le problème de partitionnement dans un graphe biparti (allocation des routes aux sommets), une telle chaîne correspond à une chaîne augmentante permettant d'étendre l'allocation courante au sommet i. Arbre de remplacement i a i b c j a b j k k c Figure 7-5 : Série d'échange de sommets pour forcer l'insertion de i Cet arbre de remplacement est exploré de manière exhaustive (mais tous les mouvements correspondant aux feuilles de cet arbre n'impliquent au plus qu'un sommet par route) et on sélectionne la feuille dont le coût total d'insertion est minimal. On notera ici que pour le problème consistant à trouver le meilleur mouvement d'optimisation locale, on a utilisé un algorithme de recherche globale, dans l'esprit de [PG 96] Résultats On présente, dans le tableau 7-2, des résultats sur des problèmes de tournées générés aléatoirement, avec 200 ou 500 nœuds, une distance Euclidienne symétrique, et en moyenne une dizaine de nœuds par tour. On compare dans le tableau suivant plusieurs méthodes : CW, l'heuristique de Clarke et Wright [CW 64], éventuellement suivie d'une passe de 3-opt (CW + 3opt), Ins, l'heuristique d'insertion décrite plus haut sans la composante d'optimisation locale incrémentale, éventuellement suivie d'une passe d'optimisation locale (Ins + 3opt), et enfin la procédure d'insertion avec optimisation locale incrémentale (O.L.I.). CW CW+2/3opt Insertion Ins. + 2/3 opt ILO N=200, pb # (9) 1 s (9) 45 s (10) 0.02 s (10) 45 s (8) 6 s. N=200, pb # (12) 1s (12) 40 s (13) 0.02 s (13) 40 s (11) 7 s. N=200, pb # (10) 1 s (10) 25 s (12) 0.02 s (12) 45 s (9) 7 s. N=500, pb # (20) 10 s (20) 750 s (22) 0.1 s (22) 900 s (17) 12 s. N=500, pb # (20) 10 s (20) 750 s (20) 0.1 s (20) 1000 s (18) 13 s. N=500, pb # (20) 10 s (20) 850 s (22) 0.1 s (22) 1200 s (17) 12 s. Tableau 7-2: heuristiques et optimisation locale sur le VRP (temps sur un PC Pentium Pro 200Mhz.) Ce tableau appelle plusieurs conclusions. Tout d'abord, la procédure des économies est meilleure en moyenne que celle d'insertion (ce que l'on avait déjà constaté sur le tableau 7-1). Ces résultats peuvent être sensiblement améliorés par une phase d'optimisation locale après la construction du tour (on améliore de 14.5% la distance totale), mais l'optimisation locale est particulièrement longue (on dépasse l'heure de calcul pour des problèmes à 1000 nœuds), ce qui la rend impropre à résoudre des grands problèmes. La vrai surprise vient de la procédure mixte d'insertion et optimisation locale incrémentale (O.L.I.) qui fournit des résultats de bien meilleure qualité : on utilise 17% de routes en moins, et le trajet total est en 10
129 moyenne 16.8% plus court (2.7% plus court que l'heuristique en deux temps: insertion puis optimisation locale complète). Ainsi, bien que l'o.l.i. n'explore qu'un petit sous-ensemble des mouvements de 3-opt (ce qui permet un gain de temps substantiel), ceux-ci sont envisagés au fur et à mesure de la construction, ce qui permet de corriger plus tôt les défauts de la solution (et amène un réel gain de performances). Ces résultats font de la procédure mixte d'insertion et d'o.l.i. une heuristique simple, générale et très efficace pour résoudre des problèmes de tournées. On va maintenant montrer comment adapter cette heuristique à deux situations particulières : d'une part, on va prendre en compte des contraintes temporelles (fenêtres de temps), et on étudiera aussi bien le problème de la minimisation du nombre de routes que celui de la minimisation de la distance. D'autre part, on montrera comment utiliser l'heuristique pour la résolution de très grands problèmes. 7.4 Cas du VRPTW On s'intéresse maintenant au cas du problème des tournées avec fenêtres temporelles, le VRPTW. De même que pour le TSP, l'addition des fenêtres de temps rend le problème beaucoup plus complexe puisque la question de la faisabilité (existence d'une solution) devient beaucoup plus difficile Adaptation de l'algorithme Comme on l'avait mentionné pour le TSPTW, les heuristiques sont parfois moins efficaces en présence de fenêtres de temps puisqu'elles doivent prendre en compte deux objectifs qui sont parfois antagonistes : d'une part minimiser le parcours et d'autre part, utiliser au mieux le temps pour éviter que le problème ne devienne irréalisable. On verra que cet équilibre devient particulièrement important lorsque l'on cherche à minimiser le nombre de routes. Adapter l'heuristique d'insertion aux fenêtres de temps est aisé : soit [a i, b i ] la fenêtre de temps du nœud i, définissons pour toutes les arêtes (x,y) de r, earliest(y) := max(a y, earliest(x) + d(x,y)) et, en visitant r en partant de la fin, latest(y) := min(b y, latest(y) - d(x,y)) (il s'agit de la propagation habituelle du PERT). L'insertion de i entre x et y est admissible si et seulement si : earliest(x) + d(x,i) < b i et max(earliest(x) + d(x,i), a i ) + d(i,y) < latest(y) On peut donc juger de la faisabilité d'une insertion a priori, en propageant les bornes earliest et latest le long de la route. Les mouvements d'optimisation locale k-opt s'adaptent moins facilement aux contraintes temporelles. En effet, une grande partie du voisinage est susceptible de devenir irréalisable, pour cause de contraintes temporelles violées. Par exemple, tous les mouvements k-opt qui change le sens de parcours de souschaînes ont de fortes chances de produire des solutions irréalisables. Ainsi, en contenant moins de solutions réalisables, ces voisinages deviennent moins efficaces, et une procédure d'optimisation locale peut plus facilement converger vers un optimum local éloigné de l'optimum global. De plus, tester si un mouvement est réalisable prend du temps : un temps constant pour les mouvements 2-opt et les mouvements de transfert de sommet, et un temps linéaire en la taille de la route pour les mouvements 3- opt qui changent l'orientation d'une chaîne de sommets. Les procédures simples d'optimisation locale k- opt deviennent ainsi moins efficaces et plus coûteuses en temps. L'approche habituelle consiste alors à proposer des algorithmes spécialisés pour traiter efficacement les contraintes temporelles dans des voisinages plus complexes [Sa 85], [So 87], [DDS 92], [KS 97]. Le schéma d'optimisation locale incrémentale que nous avons proposé peut s'adapter facilement au cas de fenêtres de temps, puisqu'aucun des mouvements considérés ne parcoure à rebrousse-poil une séquence de villes. L'ensemble des mouvements entre routes garde une bonne efficacité (on trouve souvent des 11
130 mouvements qui améliorent le coût de la solution courante), par contre, la boucle d'optimisation d'une route par 3-opt, elle, n'arrive pas toujours à trouver la solution optimale du petit TSPTW Optimisation exacte des tours Pour améliorer la procédure globale, on peut remplacer la boucle d'optimisation interne des routes utilisant 3-opt par un algorithme d'optimisation exacte. On pourrait utiliser un algorithme de programmation dynamique, mais l'algorithme de programmation par contraintes, décrit au chapitre 4, est beaucoup plus rapide (tous les problèmes à moins de 15 nœuds sont résolus en 100ms.), et capable de traiter des problèmes plus grands (on peut l'utiliser, en se limitant éventuellement à une recherche incomplète, pour des problèmes jusqu'à 30 nœuds). Comme l'algorithme de programmation dynamique, l'algorithme de programmation par contraintes est capable d'intégrer toutes sortes de contraintes ad-hoc autres que les fenêtres de temps. On fait donc appel, à l'intérieur de la phase d'optimisation locale incrémentale, à cet algorithme pour l'optimisation exacte d'une route. De plus, pour chaque sommet, quand la phase d'optimisation locale est terminée, on applique ce module pour essayer de réoptimiser la route dans laquelle le sommet a été inséré. Ce module de résolution du TSPTW peut être utilisé à d'autres moments de l'algorithme d'insertion. Ainsi, quand l'insertion de i dans r semble prometteuse mais est irréalisable (car la route r est pleine), avant de chercher à la rendre possible en transférant un morceau de r dans une autre route, on peut utiliser le module d'optimisation exacte pour chercher s'il existe une route admissible contenant tous les sommets de la route r et le nouveau sommet i. De même, dans les cas où l'algorithme d'insertion s'arrête sans arriver à insérer le sommet courant dans aucune route (et s'il ne reste plus de route disponible à créer), on peut chercher à forcer l'insertion dans les routes existantes, comme décrit précédemment. Enfin, ce module de résolution de TSPTW peut aussi être utilisé pour les insertions irréalisables. Soit pour essayer de forcer une insertion irréalisable : quand i ne peut pas être inséré directement dans r, mais que i est très proche de r, on appelle l'algorithme de TSPTW pour trouver une route parcourant l'ensemble des nœuds de r augmenté de i Résultats Le tableau 7-3 illustre l'utilisation de différentes versions de l'heuristique d'insertion avec O.L.I sur le jeu de problèmes de Solomon [So 87]. Il s'agit de 56 problèmes de tournées avec fenêtres de temps comportant 100 clients. Ces problèmes sont regroupés en 6 groupes R1, C1, RC1, R2, C2 et RC2. Pour les problèmes R1/R2, les clients sont répartis au hasard dans un carré (on utilise la distance Euclidienne), pour les problèmes C1/C2, les clients sont répartis par agrégats (clusters), les problèmes RC1/RC2 ont une distribution mixte (au hasard et par agrégats). De plus, les solutions aux problèmes R1,C1,RC1 ont des routes assez courtes (moins d'une dizaine de clients par route), alors que les problèmes R2,C2,RC2 peuvent avoir plus d'une trentaine de clients par route. On compare dans le tableau 7-3 trois versions de l'heuristique : H1 est la version simple, H2 ajoute le module de résolution exacte de TSPTW (qui remplace la boucle interne 3-opt utilisée par H1), et H3 rajoute les mouvements qui permettent de réparer les insertions irréalisables (on rajoute le mouvement d'éjection de sous-chaîne d'une route vide, et on utilise de manière plus systématique le mouvement de chaîne augmentante). Ces résultats sont comparés avec l'algorithme de recherche tabou de référence [RT 95]. Pour chaque classe de problèmes, on indique la distance moyenne par problème et, entre parenthèses, le nombre moyen de routes par problème. 12
131 H1: O.L.I. simple H2: résolution exacte des TSPTW H3:réparation des insertions irréalisables Recherche tabou de référence [RT95] R1 (12 instances) (14.75) (13.58) (12.67) (12.16) C1 (9 instances) (10.11) (10.11) (10) (10) RC1 (8 instances) (14.25) (13.0) (12.87) (11.87) R2 (11 instances) (5.91) (5.0) (4.36) (2.90) C2 (8 instances) (3.62) (3.25) (3.12) (3) RC2 (8 instances) (7.75) (6.25) (5.5) (3.37) Tableau 7-3 : heuristiques d'o.l.i. pour minimiser la distance sur les problèmes de Solomon Ce tableau appelle plusieurs remarques. Tout d'abord, la procédure d'optimisation locale incrémentale donne de bons résultats. Bien que les résultats de [RT 95] cherchent d'abord a minimiser le nombre de routes, les distances de [RT 95] peuvent servir de point de comparaison grossier : H1 fournit en moyenne des solutions dont la distance est 0.7% au dessus de [RT 95], H2 2.3% au dessus et H3 0.3% au dessous. Puisque ces heuristiques utilisent en moyenne de une à 15 secondes (PC Pentium 300 Mhz.), comparé à 1000s. (sur une Silicon Graphics 100Mhz.) pour [RT 95], elles fournissent de bons résultats pour leur temps de calcul. Si l'on compare le nombre de routes utilisées par les solutions, on remarque que H1 utilise en moyenne 2 routes de plus par problème que [RT 95], et que l'addition à la fois du module d'optimisation exact des TSPTW et des mouvements de réparation d'insertion irréalisable, permet de descendre ce chiffre à 0.85 routes. On verra au comment diminuer encore le nombre de routes utilisées Minimisation du nombre de routes Pour chercher à minimiser le nombre de routes, on peut lancer l'algorithme pour des valeurs décroissantes de K (le nombre maximale de routes autorisées). Quand ce nombre est très restreint, le problème de satisfiabilité devient plus difficile, et au cours de la procédure incrémentale, il ne faut plus chercher exclusivement à minimiser la distance, mais aussi à construire des routes dans lesquelles on garde suffisamment de place pour insérer de nouveaux sommets. Il existe de nombreuses solutions pour faire en sorte que la procédure d'insertion produise des routes suffisamment bien agencées pour faciliter les insertions postérieures. Une première possibilité consiste à affecter un coût aux temps d'attente quand on arrive trop tôt à un sommet (i.e. avant le début de la fenêtre de temps), et que ce temps d'attente est trop court pour permettre de visiter un autre sommet. Ces coûts peuvent être pris en compte de deux manières : 1. On peut les prendre en compte sous forme de pénalité dans l'évaluation du coût d'insertion 2. On peut propager ces coûts au cours des insertions et des mouvements locaux et imposer des contraintes sur les routes limitant le coût lié au temps morts. Par exemple, on peut imposer que le temps d'attente global le long de chaque route ne représente jamais plus de 10% du temps de trajet total. La première méthode a l'avantage d'être simple, mais ses résultats son généralement défaits par l'optimisation locale, puisque ces pénalités ne sont pas prises en compte dans les mouvements locaux. La seconde méthode est plus précise, mais la propagation des temps d'attente se traduit par un net 13
132 ralentissement de l'algorithme. Ces techniques liées aux temps d'attente sont relativement instables, et doivent être envisagées au cas par cas. Une autre technique plus simple consiste à guider le processus de construction pour qu'il produise des tours qui soient relativement localisés sur une zone géographique. Pour éviter qu'une route ne desserve des sommets très loin les uns des autres, on définit le diamètre d'une route comme la distance maximale entre deux sommets de la route (distincts du dépôt), et l'on pose la contrainte : Diamètre(r) = Max x, y r \{depot} d(x, y) D L'avantage d'une telle contrainte est qu'elle peut être utilisée de manière très simple avec l'algorithme d'insertion et d'optimisation locale : il suffit de la rajouter dans le module définissant la validité d'une solution. De plus, elle rend le problème plus facile (en limitant la taille des voisinages et le nombre d'insertions réalisables) tout en guidant l'heuristique vers des solutions bien adaptées pour de futures insertions. On a intérêt à considérer différentes valeurs pour la limite D au cours du processus d'insertion. Au départ, pour guider le problème de partitionnement des sommets en routes, on imposera des contraintes fortes, en considérant des valeurs faibles de D, alors que vers la fin de la construction, le partitionnement des sommets en routes est relativement fixé et le véritable objectif est l'optimisation de la distance. On partira donc de valeurs faibles de D que l'on augmentera progressivement, ce qui revient à relâcher progressivement les contraintes de diamètre au cours de la construction de la solution Recherche LDS Pour les problèmes de taille moyenne, on peut chercher à améliorer encore les résultats en rajoutant à la procédure décrite un peu de recherche globale et de retours en arrière. On va utiliser à cet effet le mécanisme de recherche LDS (Limited Discrepancy Search), proposé dans [HG 95]. Le principe de cette recherche est le suivant : on part d'une heuristique gloutonne qui construit une solution par une suite de choix, en sélectionnant (par une fonction heuristique) la possibilité qui semble la plus intéressante. Ainsi, au cours de la construction, on aura fait à certaines étapes un choix clair (car la branche choisie par l'heuristique gloutonne semble vraiment plus intéressante que les autres), alors que pour d'autres étapes, le choix aura été moins clair (d'autres branches semblaient presqu'aussi prometteuses). La recherche LDS va enrichir l'heuristique en posant un nombre limité de points de choix, précisément aux étapes où l'heuristique est la moins sûre de sa décision. Si on considère des points de choix binaires et si on limite le nombre de ces points de choix le long d'une branche à k, on va proposer des solutions dont la construction n'aura divergé de l'heuristique gloutonne qu'à au plus k étapes, et on obtiendra 2 k solutions. On peut ainsi contrôler la complexité de la recherche et parcourir l'arbre de recherche de manière très partielle. Ce type de recherche arborescente est intéressant quand on dispose de plusieurs heuristiques pour guider la procédure gloutonne : on peut alors brancher quand les heuristiques ne donnent pas le même avis; par exemple si l'on dispose d'une procédure sensible au réglage de certains paramètres, on peut comparer à chaque étape les avis de la procédure pour chacun des réglages de paramètres et brancher quand les avis divergent. Le LDS fournit ainsi une méthode générale pour rendre plus robuste une heuristique gloutonne sensible aux réglages de paramètres. Pour le cas du VRPTW, on va brancher sur plusieurs types de décisions : 1. Quand, parmi toutes les insertions réalisables, les deux meilleures ont des coûts très proches, on envisage les deux possibilités. 14
133 2. Quand un mouvement de chaîne augmentante fournit un meilleur coût d'insertion que la meilleure insertion réalisable, et que ces deux coûts sont proches, alors on envisage les deux possibilités (d'une part la meilleure insertion réalisable et d'autre part, l'insertion irréalisable suivie d'une réparation par une chaîne augmentante). 3. Quand la meilleure insertion correspond à une création de route et qu'on n'est pas arrivé à rendre possibles les insertions prometteuses mais irréalisables (dans des routes pleines) par un transfert de sous-chaîne, on essaye un tel transfert avec un réglage plus libéral des paramètres. Si on obtient un transfert qui rend possible une insertion dans une route pleine, on crée un point de choix en examinant les deux possibilités (création de route ou insertion dans une route pleine et transfert de sous-chaîne). 4. Quand l'heuristique choisit de créer une nouvelle route, on cherche si l'on aurait pas pu casser une route pleine en deux et insérer dans une des deux portions (il est parfois plus intéressant de créer un nouvelle route en en cassant une autre en deux, plutôt que créer une nouvelle route élémentaire, par insertion d'un seul client). Si l'on trouve un tel mouvement, on pose un point de choix en examinant les deux possibilités. Enfin, on considérera, pour les problèmes de Solomon à 100 nœuds, la valeur k=4, ce qui fournit au plus 2 4 =16 solutions et ralentit l'algorithme au pire d'un facteur Résultats Le tableau 7-4 présente des expériences sur la même série de problèmes de Solomon [So 87], où l'on cherche, cette fois, à minimiser le nombre de routes. On compare deux heuristiques : la première, H4 est l'heuristique d'optimisation locale incrémentale H3 utilisée pour minimiser le nombre de routes (H4 itère l'heuristique d'o.l.i. H3 pour des valeurs décroissantes de K). H5 ajoute à H4 les contraintes de diamètre et la recherche LDS. H4: O.L.I. H5: LDS référence [RT 95] R1 (12 instances) (12.67) (12.41) (12.16) C1 (9 instances) (10) (10) (10) RC1 (8 instances) (12.12) (12) (11.87) R2 (11 instances) (3.18) (3.09) (2.91) C2 (8 instances) (3) (3) (3) RC2 (8 instances) (3.37) (3.37) (3.37) Tableau 7-4 : minimisation du nombre de routes sur les VRPTW de Solomon La première de ces heuristiques H4, est très rapide (quelques secondes) et donne des résultats qui utilisent en moyenne 0.2 routes de plus que ceux de [RT 95] et 3.12% plus de distance. Si l'on rajoute les contraintes de diamètre et la recherche LDS, ces chiffres sont divisés par deux : 0.1 routes en plus et 1.5% de distance en plus, pour des temps d'exécution de l'ordre de quelques minutes (sur un PC Pentium II 300Mhz.). Si l'on restreint la comparaison en distance à l'ensemble des instances sur lesquelles on trouve exactement le même nombre de routes que [RT 95], les solution fournies par ces procédures d'insertion et 15
134 O.L.I utilisent 6.5% de distance en plus. Si l'on considère que l'algorithme de rechercher tabou de [RT 95] utilisait 1000s. (sur une Silicon Graphics à 100Mhz.), ces deux heuristiques fournissent des résultats de bonnes qualité. Enfin, mentionnons qu'une autre version de l'heuristique utilisant la même recherche LDS, et les contraintes de diamètre de manière plus limitée (sans imposer les contraintes de diamètre dans les mouvements locaux) donne en moyenne des résultats moins bons, mais a permis d'améliorer l'état de l'art sur 4 problèmes. On a ainsi obtenu les valeurs (10) sur l'instance R107, (10) sur R111, (3) sur R209 et (3) sur RC Application à des applications réelles de tournées On s'intéresse maintenant à la résolution de problèmes plus proches des application réelles de tournées (par exemple, pour la planification de tâches de maintenance sur un réseau ou pour des tournées de livraisons) qui les rendent difficiles. Ces problèmes ont généralement deux caractéristiques : leur très grande taille et le nombre de contraintes annexes qu'il comportent. On commencera par montrer que l'algorithme précédent a une complexité pratique pseudo-linéaire qui lui permet de traiter des problèmes de grande taille. On décrira ensuite rapidement quelques techniques pour prendre en compte des contraintes supplémentaires Résolution de grands problèmes L'heuristique précédente a été appliquée à des problème réels comportant 5000 clients et 300 routes, ainsi que de nombreuses contraintes additionnelles. Malheureusement, nous ne connaissons pas de benchmarks académiques de cette taille. Pour montrer que l'heuristique précédente s'applique bien à des problèmes de taille plus grande, on propose de fabriquer des problèmes de plus grande taille à partir des instances de Solomon considérées précédemment. Pour cela, on regroupe les données de tous les problèmes d'une même classe en une seul problème. Ceci nous fournit 6 problèmes comportant entre 800 et 1200 sommets. Le tableau 7-5 compare, sur ces 6 problèmes, deux versions de l'heuristique minimisant la distance : H1 est la version simple d'o.l.i. présentée précédemment, et H3 une version un peu simplifiée de la version précédente, utilisant le solveur exact en P.P.C. pour les TSPTW, ainsi que les mouvements de réparation d'insertions irréalisables (transfert de sous-chaîne et recherche de chaîne augmentante). H1 H3 tout R1 (1200 sommets) (95) 130s (96) 113s. tout C1 (900 sommets) (88) 66s (88) 66s. tout RC1 (800 sommets) (72) 96s (73) 111s. tout R2 (1100 sommets) (20) 240s (21) 251s. tout C2 (800 sommets) (28) 42s (33) 60s. tout RC2 (800 sommets) (17) 214s (17) 220 s. Tableau 7-5 : Application de l'heuristique d'o.l.i. à des grands problèmes Ce tableau appelle deux commentaires : 16
135 1. Les trois premières instances (R1, C1 et RC1) sont représentatives des problèmes réels que l'on veut résoudre (en terme de nombre de sommets par tour). L'heuristique s'adapte bien à ces problèmes dix fois plus gros puisque l'on obtient des temps d'une à deux minutes (Pentium II, 300 Mhz.). L'utilisation du module de résolution exacte des TSPTW ralentit l'algorithme au pire d'un facteur Les trois instances suivantes (R2, C2 et RC2) comportent beaucoup plus de sommets par route (jusqu'à une soixantaine). Pour garder une complexité pseudo-linéaire, on désactive le mouvement de transfert de sous-chaîne. Les temps d'exécution restent compris entre 2 et 4 minutes. Les routes comportant plus de sommets, l'apport du module de résolution des TSPTW est moins évident Traitement des contraintes supplémentaires On envisagera ici rapidement des contraintes supplémentaires suivantes : = des contraintes temporelles en plus des fenêtres de temps (planification des pauses déjeuner, synchronisation de tâches, temps de trajet dépendant de l'heure de visite), = des contraintes de spécificité des tours (par exemple, si l'on prend en compte des compétences différentes pour chacun des techniciens), = des contraintes de ressource (les tâches nécessitent certains outils). Ces contraintes ressemblent à celles rencontrées au chapitre 5 dans le problème de gestion de personnel, que l'on avait aussi résolu par un algorithme d'insertion et d'optimisation locale incrémentale. Nous décrivons ici comment les prendre en compte dans l algorithme d insertion: Pour les contraintes de ressources sur les tâches, on propose de distinguer deux cas : les petites ressources qui restent affectées au technicien tout au long de la route et les grosses ressources que le technicien ne transporte pas avec lui toute la journée et qui peuvent être utilisées successivement par plusieurs techniciens différents dans la journée. Les petites ressources peuvent être traitées comme une compétence associée au technicien (on résoudra éventuellement un problème de répartition équitable de ces petites ressources, avant la création des tournées). Pour les grosses ressources, la situation est totalement différente, et on peut remarquer qu'elles possèdent, comme les techniciens, un trajet et un horaire journalier. On traitera ces grosses ressources comme autant de techniciens supplémentaires, on dupliquera les tâches concernées (une affectée au vrai technicien, l'autre affectée au technicien représentant la ressource) et on posera une contrainte de synchronisation entre ces deux tâches. Pour les contraintes de précédence (ou synchronisation) entre deux tâches t et t', on choisit de traiter l'insertion des deux tâches en même temps : on examinera tous les couples de routes (r,r ) pour insérer les tâches (t,t ). Ceci revient à faire une exploration de profondeur 2 de l arbre de recherche (au lieu de ne considérer qu'un seul niveau de profondeur). On choisit délibérément de passer un peu plus de temps à ces insertions car les contraintes de temps gênent l optimisation locale et les choix pour les tâches liées par de telles contraintes de précédence (ou de synchronisation) peuvent ainsi difficilement être remis en cause par la procédure d'optimisation locale incrémentale. Si l'on cherche à produire pour chaque route un plan complet, prenant en compte les pauses et les repas, on sera amené, comme dans le cas de la gestion de personnel, à faire un traitement ad-hoc complexe, en représentant l'ensemble des horaires admissibles en intention plutôt qu'en extension (on raisonnera ainsi sur le nombre de pauses de l'horaire, sur leurs durées individuelles, leur durée cumulée, etc.) Enfin, pour ce qui est de l'ordre d'insertion des sommets, dans la boucle globale de l'algorithme, dans le cas où les opérateurs ont des compétences, et où ces contraintes de compétences contraignent fortement le problème, il sera utile de s'inspirer des techniques de résolution des problèmes d'emplois du temps 17
136 (modèles des requêtes pour l'allocation matricielle). Ainsi, on préférera choisir en priorité non pas uniquement les tâches les plus lointaines du dépôt, mais aussi les tâches les plus difficiles à affecter. On pourra ainsi compter le nombre de techniciens pouvant faire une tâche (c'est à dire le nombre de routes dans lesquelles on peut envisager une insertion) et sélectionner les tâches pouvant être faites par le moins de techniciens d'abord. On pourra encore raffiner ce critère par une analyse de rareté accordant moins de poids aux techniciens les plus demandés. Sur une application industrielle de grande taille, nous avons fait les mesures suivantes : en partant d un algorithme d'insertion naïf sans exploration et basé sur le first-fail, on gagne de 3 à 5% en introduisant l analyse de rareté et le lookahead sur un niveau de profondeur (O.L.I. naïve). En remplaçant (quand on choisit pour une tâche le meilleur technicien) l optimisation du tour avec 3-opt par une procédure exacte de TSPTW par contraintes, on gagne encore 2%. En rajoutant la phase d optimisation locale incrémentale complète, on peut encore gagner 5 à 10%. Il est cependant très difficile de connaître la distance à l optimum exact, car on ne dispose pas de techniques de borne inférieure. 7.6 Bilan Nous avons proposé une heuristique générale pour la résolution de problèmes de tournées, basée sur la combinaison des procédés d'insertion gloutonne et d'optimisation locale incrémentale. Cette procédure est très simple à programmer et fournit de très bons résultats pour la minimisation de la distance totale d'un plan de tournées. Cette heuristique a le double mérite d'être très efficace et très générale puisqu'elle permet de traiter des problèmes à la fois de grande taille mais aussi comportant de nombreuses contraintes supplémentaires. Nous avons appliqué cette heuristique aux problèmes de Solomon [So 87] sur lesquels nous avons obtenu de très bons résultats puisque l'heuristique trouve en quelques seconde une solution qui utilise en moyenne 0.2 routes de plus que le meilleur algorithme de recherche tabou. Nous avons proposé une variation avec de la recherche LDS capable d'améliorer ces résultats pour fournir des solutions utilisant en moyenne 0.1 routes de plus que la recherche tabou. Nous avons aussi pu améliorer l'état de l'art sur 4 problèmes parmi les 56 du jeu de test. Enfin, nous avons donné quelques indications comme quoi cette heuristique permet de traiter des problèmes réels complexes. D'une part, nous avons montré qu'elle pouvait résoudre des problèmes d'un millier de nœuds en quelques minutes. D'autre part, nous avons esquissé un certain nombre de propositions pour prendre en compte des contraintes supplémentaires. Les expériences de ce chapitre ont aussi une importance plus générale. D'une part elles confirment que la recherche limitée de type LDS est une technique générale qui permet d'améliorer un algorithme glouton quand on dispose de plus de temps pour la résolution. Pour les problèmes de grande taille, où il est hors de question de parcourir de manière complète un arbre de recherche, elle constitue un mode de visite très pertinent. De plus, le schéma général de l'algorithme combinant insertion gloutonne, recherche en avant et optimisation locale incrémentale constitue lui aussi un algorithme général, puisqu'il a été appliqué à des problèmes d'emplois du temps (gestion de personnel) ou l'on construit des emplois du temps individuels à partir d'un lot de tâches à affecter. La principale différence entre l'algorithme des tournées et celui de gestion de personnel est que le module d'optimisation sous contraintes des routes (TSPTW) est remplacé par un module d'optimisation d'emplois du temps individuel. Les mouvements locaux et la stratégie d'insertion sont les mêmes. On peut ainsi considérer cet algorithme d'o.l.i. comme une heuristique générique de résolution des problèmes où l'on cherche à affecter des tâches à des personnes. Enfin, l'algorithme que nous avons décrit est un véritable algorithme hybride puisqu'il combine recherche locale, résolution exacte de sous-problèmes en branch&bound, insertion gloutonne avec recherche en 18
137 avant, réparation de mouvements irréalisables et recherche globale limitée de type LDS. Les techniques de programmation par contraintes ont été utilisées dans de nombreux composants de cet algorithme. Ces techniques vont de la simple vérification a posteriori de contraintes (pour tester la faisabilité de mouvements locaux ou d'insertions) à l'utilisation d'un module de résolution exacte de TSPTW par optimisation en branch&bound. De plus, pour guider de manière heuristique la procédure vers des solutions utilisant peu de routes, on a posé des contraintes additionelles que l'on a progressivement relachées. Enfin, l'utilisation de raisonnement hypothétique (une possibilité offerte par les outils de P.P.C.) a été utilisée de manière systématique. Il y a ainsi deux avantages à avoir utilisé la programmation par contraintes pour un tel algorithme. D'une part, il est aisé de rajouter de nouvelles spécifications à l'algorithme: il suffit de rajouter des contraintes dans les parties de l'algorithme faisant de la propagation, sans modifier le corps principal de la procédure. D'autre part, l'algorithme peut être utilisé pour d'autres problèmes consistant à affecter des tâches à des personnes (comme le problèeme de gestion de personnel mentionné ci-dessus), simplement en remplaçant le module de propagation par un autre. 19
138
139 21
140 8. Chapitre 8. Bilan Nous allons conclure cette partie A en faisant le bilan des expériences algorithmiques sur les problèmes combinatoires étudiés. La première question du sujet de la thèse était : peut-on utiliser la programmation par contraintes pour résoudre ces problèmes combinatoires?. Nous allons rassembler les réponses obtenues en présentant tout d'abord une synthèse des techniques qui se sont avérées efficaces pour la résolution de ces problèmes (la cartographie des techniques de résolution que l'on s'était fixée comme objectif). En revenant sur les problèmes pour lesquels l'approche par contraintes a été un succès, nous examinerons les techniques qui ont permis de passer d'approches naïves en P.P.C., simples à programmer mais généralement inefficaces, aux algorithmes de P.P.C. plus complexes et efficaces. Nous examinerons ensuite les possibilités ouvertes par la combinaison de la P.P.C. avec d'autres techniques algorithmiques. Nous verrons combien les algorithmes hybrides constituent une piste riche pour le développement d'algorithmes efficaces. Enfin, nous verrons quelles perspectives cette étude ouvre pour la mise en œuvre d'algorithmes complexes d'optimisation, avant d'aborder ce sujet dans la partie B. 8.1 Cartographie de la résolution de problèmes combinatoires On résume ici, de manière synthétique et graphique l'ensemble des expériences menées au cours de cette étude. Pour chacune des familles de problèmes étudiées, on établit une petite cartographie des techniques de résolution. On reporte les types de problèmes sur un diagramme à deux dimensions, où l'axe vertical représente la taille du problème et l'axe horizontal la progression du problème pur à des problèmes dérivés (comportant des contraintes supplémentaires). Sur ce diagramme, on indique la limite d'utilisation : = des techniques exactes, représentée par un trait continu, = des techniques approchées, représentée par un trait pointillé, de plus en plus serré à mesure que les solutions retournées sont proches de l'optimum = des techniques utilisant la programmation par contrainte, par un trait grisé. Ces diagrammes sont à but illustratif. Les données qui y sont reproduites sont empiriques et sont simplement le reflet des expériences que nous avons menées. Cependant, ils permettent une première évaluation des techniques de Programmation par Contraintes (plus ou moins sophistiquées) par rapport aux techniques habituelles de Recherche Opérationnelle. Bien entendu, ces graphiques sont des ébauches et seraient à compléter indéfiniment. On commence par présenter sur la figure 8-1 une comparaison des techniques pour l'allocation bijective de ressources. On voit que la propagation par contraintes, offre une solution simple à programmer pour des petits problèmes (jusqu'à 40 nœuds). Pour de plus grands problèmes, il faut enrichir cet algorithme de propagation par un calcul incrémental de borne inférieure utilisant la méthode Hongroise. 1
141 taille du problème méthode Hongroise flots (impl. simple) PPC soignée 2 10 Allocation bijective de poids maximal PPC naïve Allocation avec remplacement de ressources Allocation bi-critère Contraintes additionnelles Figure 8-1: Allocation bijective de ressources La figure 8-2 fait la synthèse des algorithmes envisagés pour la résolution de problèmes d'ordonnancement. On remarque que le problème du job-shop est beaucoup plus facile que les variations à ressources capacitives (cumulatives ou non). On note que la propagation de contraintes peut être utilisée de manière efficace dans de nombreux algorithmes de résolution, exacts ou approchés. taille du problème (nombre de tâches) Insertion gloutonne (construction chronologique) Recherche tabou (vosinages simples) Shuffle et PPC PPC naïve LDS et PPC PPC soignée (edge finding) Contraintes additionnelles Job-Shop Precedences complexes Tâches préemptives Ressources n-aires (RCPSP) Ressources cumulatives Cumulatif (semi-)préemptif Figure 8-2: Ordonnancement (planification temporelle sous contraintes de ressources) La figure 8-3 présente une synthèse de techniques pour la résolution de problèmes d'optimisation de parcours, le voyageur de commerce et ses généralisations aux problèmes de tournées. 2
142 taille du problème (nombre de clients) Insertion gloutonne 2-opt Branch & Bound(cas asy métrique) 70 PPC + borne Rel. Lag. Branch & Cut LDS et O.L.I. Insertion et O.L.I TSP PPC soignée PPC naïve TSPTW VRP VRPTW Tournées sous contraintes Contraintes additionnelles Figure 8-3: optimisation de trajet (voyageur de commerce et tournées) 8.2 P.P.C. et Optimisation Combinatoire Pertinence de la P.P.C. Si l'on cherche maintenant à faire le bilan de l'utilisation de la programmation par contraintes sur l'ensemble des problèmes, on peut considérer que celle-ci est compétitive avec les meilleures techniques (puisque des records on été battus ou des problèmes ouverts résolus), sur les problèmes suivants : 1. l'ordonnancement disjonctif 2. les problèmes de voyageur de commerce (TSP) de petite taille (moins de 15 nœuds) 3. les problèmes de tournées avec fenêtres de temps (VRPTW) Sur d'autres problèmes, l'approche par contraintes apporte aussi de très bons résultats par rapport à d'autres algorithmes, mais nous n'avons pas pu la comparer à un état de l'art bien établi. Ainsi en est il pour : 1. les problèmes d'ordonnancement cumulatif, 2. les problèmes d'emplois du temps prenant en compte les préférences du personnel Des résultats plus décevants ont en revanche été obtenus sur 1. les grands problèmes de couverture d'ensembles (l'approche linéaire avec éventuellement de la relaxation Lagrangienne donne de bien meilleurs résultats), 2. les problèmes d'allocation bijective pure (la méthode Hongroise est bien plus efficace), 3. les problèmes qui se traduisent facilement en termes de flots maximaux (comme par exemple la relaxation du problème de gestion de personnel), 4. les problèmes d'optimisation de parcours sans contraintes annexes (pour lesquels le branch and cut fournit d'excellentes solutions). 3
143 Au total, les succès pèsent lourd dans la balance. De plus ces algorithmes par contraintes offrent le grand avantage d'être flexibles et de pouvoir facilement prendre en compte des contraintes annexes (puisqu'il suffit de rajouter quelques règles de propagation). On a ainsi adapté facilement le programme original de P.P.C. pour les couplages bipartis aux cas d'allocations contraintes (bi-critère ou remplacement de machines), ainsi qu'au problème du voyageur de commerce et à celui des tournées. Il y a donc véritablement réutilisation d'algorithmes de contraintes, alors que l'adaptation d'algorithmes spécialisés de Recherche Opérationnelle s'est montré beaucoup plus problématique (introduction de relaxation Lagrangienne, etc.) Ainsi on peut conclure que la programmation par contraintes est une technique très pertinente pour la résolution de problèmes combinatoires, tant par ses qualités de flexibilité et de modularité qui la rendent bien adaptée à des problèmes réels, que par ses qualités d'efficacité qui rendent possible la résolution de problèmes difficiles et de grande taille Conditions de mise en œuvre de l'approche par contraintes Nous allons maintenant essayer de revenir sur les points techniques qui nous ont permis d'obtenir ces succès avec la P.P.C. et essayer d'en dégager des conclusions générales sur les choix techniques qui se posent aux concepteurs de systèmes de contraintes aussi bien qu'à leurs utilisateurs. La quasi-totalité des algorithmes présentés font appel, dans leur schéma de propagation, à des structures intermédiaires, comme le modèle dual des potentiels (allocation de ressource), un arbre couvrant (voyageur de commerce), les intervalles tâches (ordonnancement disjonctif), un histogramme de ressource (ordonnancement cumulatif), des requêtes de placement d'activités (emplois du temps), etc. Ce sont ces structures intermédiaires qui font la force des algorithmes de contraintes (on a bien vu la faiblesse d'implémentations naïves, sans contraintes redondantes), car elles permettent de proposer des règles de propagation complexes. Cependant, ces structures font appel, pour être mises à jour de manière incrémentale, à des algorithmes complexes qui s'expriment le plus aisément par une procédure impérative. Pour pouvoir programmer les règles de propagation d'un système de contraintes dédié à la résolution de problèmes combinatoires discrets, il faut donc avoir un langage qui permette : = d'écrire simplement des algorithmes impératifs classiques, = d'utiliser des structures de données intermédiaires plus riches que les simples variables de domaine, et sur lesquelles on puisse faire du raisonnement hypothétique revenir en arrière. On remarque aussi que les arbres de recherche envisagés ont souvent fait appel à des procédures plus complexes que le seul choix d'une variable à instancier, sélectionnée sur le seul critère first-fail. On s'est en effet servi de ces structures intermédiaires pour poser des alternatives. Enfin, les arbres de recherche construits n'ont pas toujours été de simples arbres de recherche en branch&bound, on a été amené à utiliser d'autres techniques comme l'apprentissage des échecs, l'analyse des conflits d'une solution ordonnancée au plus tôt, les règles de dominance ainsi que des parcours en largeur limités comme technique de propagation (shaving) ou d'évaluation heuristique (recherche par gradient d'entropie maximal). Pour pouvoir programmer de telles procédures, il faut avoir un langage qui permette : = de spécifier les points de choix qui vont être posés = de spécifier la manière dont l'arbre va être parcouru, à la fois pour la persistance d'information d'une partie de l'arbre à l'autre (apprentissage des échecs, etc.) comme pour le mélange de parcours en profondeur d'abord et en largeur d'abord. On remarquera que les systèmes de P.P.C. basés sur Prolog (clp(fd), Chip [BC 94],...) ne répondaient à aucun de ces critères (ce qui empêche ainsi les utilisateurs de les étendre par de nouveaux mécanismes dédiés à des contraintes globales particulières). Les systèmes basés sur la notion de contraintes 4
144 concurrentes (comme cc(fd) [vhsd 93], Oz [Sm 95], etc.) d'une part permettent de programmer des règles d'inférence complexes et d'intégrer des procédures impératives au code (mais sont encore limités par la nature de l'information que l'on peut défaire) et d'autre part, ont suivi la direction de recherche consistant à offrir un formalisme pour la programmation de procédures de recherche [SS 94]. 8.3 Algorithmes hybrides Si l'on reprend l'ensemble des algorithmes par contraintes qui ont été proposés dans cette première partie, la plupart d'entre eux sont difficilement implémentables de manière directe dans le cadre PLC, tel qu'il avait été défini au début de la programmation par contraintes [JL 87]. On peut en effet considérer qu'une partie des algorithmes proposés ne sont plus stricto sensu des algorithmes de P.P.C. mais plutôt des algorithmes hybrides combinant la P.P.C. avec d'autres techniques algorithmiques. On a été ainsi amené à élargir le cadre habituel de la P.P.C. de plusieurs manières : 1. Pour les grands problèmes dont la taille interdit toute résolution exacte, on a été amené à faire des recherche incomplètes plutôt qu'exhaustives. Dans ces cas, on a le plus souvent spécifié de manière assez fine la partie de l'arbre de recherche que l'on voulait visiter. Dans les cas les plus simples, on a imposé une limite au nombre de backtracks autorisés. Dans des cas plus complexes, on a imposé des conditions topologiques sur la forme de l'arbre visité (limitant le nombre de branches visitées à partir d'un nœud, limitant la recherche en avant). L'exemple le plus frappant est la recherche LDS où l'on choisit très précisément ceux des nœuds de l'arbre de recherche où l'on accepte de considérer plusieurs branches. 2. On a aussi souvent été amené à combiner la propagation de contraintes à la recherche locale. Ces combinaisons ont pu prendre différentes formes. On peut en effet combiner ces techniques de manière séquentielle, en appliquer une cherche locale après ou avant une recherche globale. On peut aussi faire coopérer de manière plus étroite les deux paradigmes, par exemple en appliquant la recherche locale à chaque feuille de l'arbre de recherche globale, ou en effectuant une recherche globale en P.P.C. pour trouver chaque mouvement d'optimisation locale (procédure shuffle en ordonnancement, procédure de Lin et Kernighan pour le TSP) ou encore en faisant un peu de recherche locale à chaque nœud de la recherche globale, sur des solutions partielles (optimisation locale incrémentale pour le problème des tournées). Il s'agit donc véritablement d'algorithmie hybride, puisqu'on mélange une méthode basée sur la transformation de solutions (l'optimisation locale) à une méthode basée sur la construction de solutions par raffinements (propagation de contraintes). 3. Pour les problèmes d'optimisation combinatoire pour lesquels la Recherche Opérationnelle fournit de bons algorithmes incrémentaux (méthode Hongroise, flots, arbre couvrant de poids minimal, etc.), on a été amené à les utiliser comme techniques de bornes dans des problèmes plus complexes. Il s'agit donc d'une combinaison entre P.P.C. et d'autres algorithmes. Le fait que nous ayons été amené à développer de telles combinaisons n'est pas très surprenant. La P.P.C. a maintenant fait ses preuves et peut être considérée avec les algorithmes spécialisés de Recherche Opérationnelle quand on a un problème combinatoire à résoudre. Puisque ces techniques ont différentes qualités et différentes faiblesses, il est naturel de chercher à les combiner pour tirer parti des avantages de chacune des méthodes. 8.4 Perspectives Aujourd'hui, deux cas se présentent à qui souhaite résoudre un problème combinatoire donné : soit le problème peut être résolu directement par une des grandes méthodes de recherche opérationnelle (algorithme de graphe, approche polyhédrale, etc.). Soit le problème en question comporte des aspects 5
145 spécifiques qui le rendent différents des problèmes étudiés dans les livres. Sa résolution relève encore d'une démarche expérimentale. Pour que cette activité d'algorithmique créative puisse un jour devenir une discipline d'ingénieurs, il faut pouvoir formaliser et encadrer le processus de conception et d'évaluation d'algorithmes. En particulier, il faut avoir à sa disposition une boite à outils algorithmique permettant d'implémenter et de combiner les techniques les plus éprouvées. Une conclusion de cette première partie est que les techniques de propagation de contraintes forment une des composantes importantes qui devraient constituer une telle boite à outils. Quels sont les enjeux liés à la mise à disposition d'une telle boîte à outils? Quelle forme devrait elle prendre (bibliothèque d'algorithmes, langage, environnement de génération de code, etc.)? C'est l'objectif de la partie suivante que de commencer à y répondre. 6
146 7
147 Partie B: Génie logiciel des algorithmes hybrides en optimisation combinatoire 1
148 9. Chapitre 9. Un langage pour les algorithmes hybrides : programme de recherches. La première partie de cette thèse s est achevée sur la conclusion de la pertinence des algorithmes hybrides pour résoudre des problèmes d optimisation combinatoire complexes. Une question qui suit naturellement ce constat est celle de la mise en œuvre de tels algorithmes, et nous allons voir qu elle pose de véritables problèmes de génie logiciel. En effet, la simple programmation d un algorithme d optimisation pur (c est à dire ne faisant appel qu à une seule technologie) est déjà considérée comme suffisamment difficile pour que de nombreux systèmes et langages spécialisés aient été proposés. Citons, par exemple, les langages AMPL [FG 93] pour la programmation linéaire, la famille des langages CLP pour la programmation par contraintes et Localizer [MvH 97] pour les algorithmes d optimisation locale. Aujourd hui, aucun système d optimisation ne permet de combiner de tels algorithmes. La seule solution offerte au développeur consiste à utiliser non plus des systèmes intégrés (des langages dédiés), mais des librairies, le plus souvent en FORTRAN, C ou C++, et à décrire l algorithme principal dans ce langage hôte. Cette situation a le double inconvénient d être restrictive (les interfaces de ces librairies sont généralement limitées et permettent rarement une coopération fine entre les composants algorithmiques) et peu satisfaisante car le faible niveau d abstraction des langages demande un travail d ingénierie conséquent au programmeur pour passer d une description en pseudo-code à une implémentation effective. Ainsi, on aboutit souvent à des systèmes complexes et difficiles à maintenir. La direction de recherche que nous exposons dans cette partie consiste à proposer un langage de haut niveau qui permette de décrire tous les aspects d un algorithme hybride. Le premier pas dans cette direction a été la réalisation du langage CLAIRE à partir de 1994, dans lequel nous avons rassemblé tous les concepts qui nous semblaient essentiels à la programmation de tels algorithmes. En 1994, il y avait deux écueils possibles : d'une part l'objectif de simplicité que nous nous étions fixé pouvait nous faire laisser de côté des concepts importants qui auraient par la suite manqué au langage, d'autre part, l'objectif de lisibilité (permettre l'écriture de pseudo-code exécutable) était difficle à atteindre (rien ne garantissait que du code que nous trouvions lisible et clair, soit facile à comprendre pour des tiers). Après trois ans d utilisation, CLAIRE a évité ces deux écueils avec succès. En effet, le langage nous a permis de mener à bien toutes les expérimentations que nous voulions, en peu de temps (ce qui aurait été inenvisageable en C++, par exemple) et cette productivité témoigne qu'il ne nous a manqué aucun concept important dans le langage. D'autre part, le langage a été utilisé en tant que pseudo-code comme support de cours en optimisation combinatoire et des élèves ont pu comprendre sans difficulté des programmes CLAIRE sans avoir été formés préalablement au langage. Toutefois, ce succès peut être légèrement nuancé : CLAIRE n'est pas encore le système idéal car le code écrit en CLAIRE ne reflète pas toujours la seule intention originale de l algorithme mais est parfois encombré de détails d implémentation de trop bas niveau. Ainsi, le langage a un peu évolué au cours de ces trois années en s enrichissant de nouvelles notions permettant d augmenter le niveau d abstraction du code. Par ailleurs, nous avons éprouvé le besoin de développer deux librairies au dessus de CLAIRE pour gagner en abstraction dans la description d'algorithmes complexes. La première librairie, ECLAIR [LDJS 98], permet de programmer des algorithmes de contraintes sur les domaines finis de manière déclarative. La seconde, SaLSA [CL97e], permet de spécifier de manière très concise le flot de contrôle d algorithmes de recherche globale, locale ou hybride, sans avoir à gérer à la main les manipulations de retours-arrière. Pour présenter ce projet de recherche à long terme qu est l expression élégante d algorithmes complexes d optimisation, nous allons proposer cinq axes de recherche. Ce projet étant très ambitieux, nous sommes loin d avoir une proposition satisfaisante pour chacune de ces problématiques. Ceci dit, pour chacun de 3
149 ces thèmes, nous présenterons rapidement les directions que nous avons envisagé. Ces axes de recherche sont les suivants : 1. Modélisation d un problème. 2. Utilisation de structures de données spécifiques pour des algorithmes. 3. Spécification déclarative de certains comportements. 4. Spécification globale du flot de contrôle d algorithmes hybrides. 5. Environnement de développement pour de tels algorithmes Sur ces cinq sujets, nous estimons avoir une réponse convenable à la question 1, une réponse solide quoique préliminaire à la question 4, de nombreux éléments de réponse à la question 2, quelques éléments de réponse à la question 3 et quelques idées à la question 5. Dans cette partie B, après avoir rapidement survolé ces cinq questions, nous approfondirons la question 4, en décrivant notre proposition, le langage SaLSA. Enfin, on rappelle que ce travail sur le langage est fait pour réaliser des algorithmes efficaces. L'optique dans laquelle nous travaillons est donc celle d une montée de l abstraction des descriptions d algorithmes, sans perte notable de performances. Nous ne retenons ainsi parmi les techniques élégantes de programmation uniquement celles pour lesquelles nous savons produire un code C++ semblable (en lisibilité et en efficacité) à celui qui aurait été produit à la main, si l'algorithme avait été programmé directement en C++. Cette contrainte forte limite le cadre que nous sommes capables de proposer, mais a l'avantage de nous éviter d'avoir à revenir en arrière sur certains choix du langages, psuique tous sont implémentés de manière efficace. 9.1 Un langage de modélisation et de simulation La première phase lors de la résolution d un problème est sa modélisation. Les problèmes réels ont généralement une structure naturelle, comportant des objets physiques, et souvent il en existe déjà un modèle informatique (par exemple une base de données). Cette première étape consiste donc à proposer un modèle formel des acteurs d une situation et de certains de leurs comportements. Si l on oublie un instant le domaine de l optimisation, les technologie les plus fréquemment utilisées dans de telles situations sont des méthodes de modélisation par entités et relations qui donnent ensuite lieu à des implémentation dans des langages orientés objets. Cette problématique de représentation est au cœur des préoccupations de CLAIRE depuis le début, puisque le premier ancêtre de CLAIRE, LORE [Ca 87] était un langage de représentation de connaissances. On va aborder ici trois points de technologie qui nous semblent essentiels pour résoudre ce problème de modélisation et nous verrons ensuite les solutions que nous avons adopté dans CLAIRE avec l utilisation que nous en faisons Modélisation des données par des objets et des relations On veut pouvoir décrire les composantes d un problème comme des entités organisées de manière hiérarchique. Le formalisme orienté objets est donc particulièrement adapté. Cependant, il en existe de nombreuses variantes (langages de «frames» ou langage objets, héritage basé sur les classes ou sur les «prototypes», héritage simple ou multiple). Pour choisir un modèle parmi tous ceux proposés dans la littérature, nous avons suivi deux objectifs : simplicité et universalité. En effet, les situations à décrire en optimisation sont la plupart du temps relativement simples, et la plupart des formalismes à objets sont capables de modéliser de telles situations. De plus, comme les logiciels d optimisation utilisés sont 4
150 amenés à être utilisés dans un environnement hétérogène, il vaut mieux que leur modèles de données soit compatible avec ceux des autres composants logiciels (comme la base de données, etc. ). Le cadre que nous avons choisi dans CLAIRE est le suivant. Chaque classe est définie par héritage simple et ajoute à la classe parent une liste de champs typés. Les champs peuvent avoir une valeur par défaut, y compris une valeur spéciale représentant l inconnu. Dans cette optique de modélisation, les types sont donnés par le programmeur pour structurer sa description du problème. Nous proposons donc d utiliser un système de types concrets, basé sur la hiérarchie des classes. Une classe est donc un type, et l héritage induit une relation de sous-typage. La simplicité du cadre objet peut être complémentée par un système de types riches. On peut ainsi introduire comme types des intervalles, des ensembles définis en extension, l union ou l intersection de deux types, des ensembles typés, des n-uplets typés (tuples), etc. En particulier, le traitement des ensembles et des listes est facilité, ce qui permet de modéliser aisément des relations entre un objet et une collection d'objets (par exemple, une classe pour avoir comme champ un ensemble d'objets d'une autre classe). Les champs des objets permettent ainsi de définir des relations entre une classe et n importe quel type. Pour permettre de modéliser des cas plus complexes, on offre aussi la possibilité de définir des relations plus générales, (par exemple des relations binaires d un type quelconque vers un type quelconque) que l on appelle des tableaux étendus. Ces relations peuvent être implémentées efficacement, soit à l aide de tableaux (pour les entiers), soit à l aide de tables de hashage. Ce modèle est est relativement simple, et le fait de le mentionner pourrait sembler être une banalité. Cependant, si l on considère les systèmes permettant de développer des algorithmes hybrides, l aspect modélisation est généralement peu traité. Par exemple, les langages CLP, basés sur Prolog, offrent comme seules structures de données les listes et termes non typés, ce qui pose à l'usage de vrais problèmes de compréhension, sécurité et maintenance [Lab 97]; C++, lui, offre un modèle objet complexe, mais n offre pas de moyen d exprimer simplement des relations multi-valuées. Le cadre de CLAIRE, bien que très simple, ne nous a pas limité depuis 1994 lorsque nous avons eu à modéliser des problèmes. On peut se poser une autre question qui est celle de l adéquation du modèle objet à la programmation d algorithmes particuliers nécessitant des structures propres (en particulier, l héritage simple n est il pas trop limitant?). Ce problème sera abordé un peu plus loin, au paragraphe Utilisation de code impératif pour décrire des méthodes sur ces objets Une fois le modèle de données choisi, il reste à simuler des comportements avec ce modèle. On a le choix entre de nombreuses possibilités pour représenter ces interactions entre objets. On peut utiliser des contraintes, des règles, des invariants, et probablement de nombreux autres formalismes. Nous commencerons pas examiner ici le mode d interaction le plus simple (et le plus consensuel) entre objets : l envoi de message et l exécution de méthodes définies par des instructions impératives (l utilisation d autres formalismes sera évoquée dans la section 9.3). Ce mode d'interaction offre en effet deux grands avantage. D'une part, le formalisme impératif (avec affectation) est utilisé pour la description d'une grande majorité d'algorithmes existants et correspond souvent aux représentations naturelles des programmeurs. D'autre part, on retiendra du modèle d'envoi de messages, deux mécanismes, le polymorphisme et la surcharge, qui, en permettant de n'utiliser que peu de noms, contribuent à la clareté du code. Par rapport aux langages de programmation traditionels, nous avons éprouvé le besoin de rajouter une extension significative : l utilisation d expressions ensemblistes. En effet, de même qu il est souvent utile dans la modélisation des données de mettre un objet en relation avec un ensemble d autres objets, on a souvent envie, lorsque l on décrit un comportement, de parler d ensembles d autres objets vérifiant certaines conditions. Quand on veut effectuer une certaine action sur un tel ensemble, on évite 5
151 généralement de le construire effectivement, pour générer les éléments de l'ensemble à la volée. Le codage de l'itération de ces ensemble implicites par des boucles, des tests et d'autres instruction est généralement facile (les programmeurs le font sans s en rendre compte), mais nuit à la lisibilité de l algorithme programmé. En effet, il conduit à mélanger dans le code des instructions purement liées à une itération avec d'autres instructions responsables du véritable traitement effectué au cours de l'itération. Nous avons étendu de plusieurs manières les instructions classiques d un langage de programmation impératif : = la boucle for fait une itération sur un ensemble quelconque au lieu d un simple intervalle d entiers. De manière similaire à [Apt 97], on propose aussi trois nouvelles instructions permettant de faire des itérations : = some(x in E P(x) ) itère l ensemble E jusqu à trouver un élément x tel que l expression Booléenne P(x) soit vraie et renvoie cet élément, ou bien la valeur unknown si aucun x n a été trouvé, = exists(x in E P(x) ) renvoie un Booléen indiquant s il existe un élément x de E vérifiant P(x), = forall(x in E P(x) ) renvoie un Booléen indiquant si tous les éléments x de E vérifient P(x). = On peut utiliser des expressions ensemblistes directement dans le langage, à l intérieur d itérations, mais aussi comme paramètres de fonctions, etc. Ces expressions, inspirées par les langages SETL [SDD 86], ont une syntaxe mathématique naturelle : = list{x in E P(x) } filtre la collection E par la condition Booléenne P(x), et {x in E P(x) } fait de même en éliminant les objets en double, = list{f(x) x in E} construit l image de la collection E par l expression f, et {f(x) x in E} fait de même en éliminant les images en double. A l usage, ces expressions se sont avérées très utiles en améliorant grandement la lisibilité du code. Elles sont donc importantes pour programmer du code impératif. Mentionnons ici que l utilisation des bibliothèques habituelles C++ ou Java (Microsoft, Watcom, Roguewave, STL, JDK) pour de telles expressions est non seulement verbeuse et inélégante, mais aussi hasardeuse. Bien que très utilisées, ces bibliothèques n'ont pas toujours le niveau de qualité que l'on pourrait attendre et la perte de perfomance par rapport à CLAIRE atteint parfois plusieurs ordres de grandeurs [Ca 97] Animation d un modèle : mondes hypothétiques Enfin, le dernier aspect qui nous parait important dans la modélisation d un problème est la possibilité d'animer ce modèle, c est à dire de pouvoir reproduire des processus d évolution du système. La simulation propre de cette évolution peut être facilement faite au moyen d une séquence d envois de messages entre objets. La difficulté d'une telle animation vient de la possibilité (que l'on aimerait avoir) de faire plusieurs exécutions (si le premier scénario ne convient pas, en essayer un autre). Le mécanisme qu il faut fournir à l utilisateur est ainsi un mécanisme lui permettant de défaire ce qu il a fait. La question qui se pose est ainsi la suivante : de quelle manière le système peut il avoir évolué depuis le début d une simulation. Les relations entre les objets peuvent avoir été modifiées, et de nouveaux objets peuvent éventuellement avoir été crées. Ceci implique pour chacune des modifications du système que l on garde en mémoire l ancienne valeur. On peut offrir plusieurs mécanismes à l utilisateur pour implémenter ces «intructions défaisables». On peut, par exemple, offrir une syntaxe spéciale d affectation. L ennui de cette solution est que l on ne peut pas utiliser le même code pour dénoter une simulation défaisable et une simulation définitive (sauf au prix de ralentir cette seconde). 6
152 La solution que nous avons choisi dans CLAIRE est la suivante : au moment de la déclaration des structures de données, on peut préciser que certaines structures doivent être stockées de manière spéciale, pour éviter d écraser les anciennes valeurs lors d une affectation. Cette possibilité est offerte en CLAIRE sur toutes les relations ainsi que sur les variables globales. On en propose aussi une version incrémentale pour les listes (pour éviter de garder toute la liste en mémoire quand un seul des éléments à changé). Ce mécanisme pourrait être étendu à des classes, dont les instances créées dans une branche hypothétique devraient être supprimées lors d un retour à un état antérieur du système). Pour permettre le retour en arrière, on organise l historique de la simulation (c est à dire du calcul) en «mondes» qui sont numérotés par des entiers. Quand on veut se souvenir d un état auquel on est susceptible de revenir plus tard, on appelle la primitive world+() qui fige l état courant du système dans un monde n et passe au monde n+1. Les évolutions suivantes auront lieu dans le monde n+1 et pourront être défaites par un appel à world-() qui reviendra au monde n. Les mondes (ou versions du système) sont organisés dans une pile, ce qui permet de faire des simulations correspondant à des parcours en profondeur d abord d arbres de possibilités (on abordera dans les chapitres suivants la question d éventuels parcours en largeur d abord). 9.2 Structures de données algorithmiques additionnelles Une fois le problème décrit par des objets et des relations, on est amené, pour implémenter des algorithmes, à décrire d autres objets ou d autres structures. Ces objets sont souvent des conteneurs d une forme ou d une autre (liste chaînées, tables de hash, arbres de toutes sortes, tas, piles, files, etc...) ou des objets spécifiques au modèle d un algorithme (par exemple, un modèle linéair dual, un sous graphe induit -MST, couplage-, un ensemble d intervalles de tâches, un arbre d intervalles de temps, un histogramme de consommation, etc. ). La manipulation de ces structures pose un certain nombre de problèmes de programmation : nous allons en décrire quatre que nous avons identifiés et pour chacun d entre eux nous présenterons rapidement les premières solutions que nous avons imaginées : 1. le premier problème est lié aux conteneurs. Ces structures sont utilisées régulièrement dans de nombreux algorithmes. Pour permettre une programmation réutilisable, il faut être capable de décrire ces structures et leurs comportements de manière paramétrique 2. le second problème est encore lié à la programmation de ces structures de conteneurs. Pour augmenter le niveau d abstraction et la modularité des programmes, on aimerait pouvoir les manipuler comme des ensembles, en ignorant la manière dont ils sont implémentés 3. le troisième problème vient de la volonté de concilier haut niveau d abstraction et efficacité. Pour être élégant, on est souvent amené à considérer des structures complexes, que l on ne construit pas de manière effective pour des raisons d efficacité. Nous proposons une solution pour pouvoir continuer à désigner ces structures implicites sans perdre en efficacité. 4. Enfin, l utilisation efficace de structures algorithmiques ne peut pas toujours se faire par l utilisation d objets extérieurs, il faut parfois modifier les objets provenant de la modélisation même du système pour leur rajouter des capacités algorithmiques. Ce constat pose un problème d encapsulation et de modularité Paramétrisme L utilisation des structures algorithmiques habituelles implémentant des conteneurs pose un problème d implémentation. D une part, on veut décrire ces structures de manière générique. D autre part, on aimerait que le système de type soit efficace sur ces structures et qu il puisse faire un peu d inférence 7
153 (détecter qu un objet qui vient de sortir d un conteneur d objets A est de type A) et de vérification (interdire qu on stocke dans un conteneur d objets A un objet B si B n est pas un sous type de A). Cette double exigence de généralité et de précision a été satisfaite en CLAIRE par la double introduction de classes paramétrées (l équivalent des templates en C++, dont on remarquera qu ils sont principalement utilisés pour la programmation de conteneurs, dans la librairie STL [SL 94]) et des déclarations de types du second ordre. Une classe peut être paramétrée par un sous ensemble de ses champs, on peut alors faire référence au sous ensemble des instances de cette classe dont les champs concernés sont de certains soustypes du domaine de valeur. Par exemple, on peut déclarer des piles génériques et considérer ensuite les piles de nombres. stack[of] <: object(of:type, contents:list, index:integer=0) stack[integer U float] La déclaration d un type du second ordre pour une méthode revient à préciser le type du résultat en fonction des types de ses arguments [CP 92]. On peut aini préciser le type de la méthode quicksort de la manière suivante, en utilisant X comme variable de type : qsort : function[domain=tuple(x,x), range=boolean], list[x] -> list[x] De nombreux systèmes de types comportant du polymorphisme paramétrique ont été proposés [ACC 93], [MBL 97]. Cependant, la combinaison du sous-typage et des variables de types est délicate et ce domaine de recherche est encore ouvert Itérateurs De même qu il est particulièrement agréable d utiliser des expressions ensemblistes pour faire des itérations dans une boucle for ou par une expression Booléenne quantifiée (exists, forall), l utilisation transparente de ces conteneurs, comme s il étaient stockés comme des ensembles, permet de rendre la description d une itération indépendante de l implémentation du conteneur, et ainsi, plus lisible [Lab 96]. Pour permettre cela, on offre en CLAIRE la possibilité de spécialiser la méthode d itération générique iterate sur toute nouvelle classe (en fait sur n importe quel type). Le petit exemple suivant montre un exemple de codage de petits sous-ensembles par des vecteurs de bits : MySet[of] <: thing(of:class, contents:integer = 0) iterate(v:variable, x:myset, e:any) => let l := x.of.instances in for i in (0.. min(31, length(l))) if x.contents[i] (v := l[i], e) Ainsi, si Task est une classe dont la liste d instances est (t1, t2, t3, t4, t5, t6, t7, t8, t9, t10), l objet S représente l ensemble (t1, t3, t7, t9) (le code binaire de 325 est On peut alors écire l expression Booléenne existentielle qui suit. S :: MySet(of = Task, contents = 325) exists(t in S t.duration > 5) Cette expression génèrera le code suivante (on remarque que cette définition d itérateurs permet de faire de la génération de code de manière puissante) : 8
154 let v:task := unknown, result:task := unknown, l := Task.of.instances in (for i in (0.. min(31, length(l))) if S.contents[i] (v := l[i], if (v.duration > 5) (result := v, break()) ), result) La restriction des itérateurs aux boucles for est un mécanisme moins puissant que les mécanismes d itérateurs proposés, par exemple, pour le langage Sather [MO 95]. Cependant, il présente deux avantages majeurs. D'une part, il permet d'obtenir un code très simple et très lisible. D'autre part, il est plus efficace que les bibliothèques d'objets génériques qui font appel à des structures intermédiaires [Ca 97]. Les bibliothèques de gestion d'ensembles, de structures de conteneurs et d'itérateurs utilisent généralement des cellules pointant sur les objets et sont donc pénalisées par des indirections supplémentaires. De plus, la génération de code permet aussi d'éviter certains appels fonctionnels (par des mécanismes de substitution inline). Le bilan de l'utilisation de ces itérateurs est donc très positif puiqu'il nous a permis d'améliorer grandement la lisibilité du code sans subir la moindre perte de performances. Jusqu à présent nous n avons pas ressenti le besoin d adopter de mécanisme plus général Foncteurs de classes / objets algorithmiques Ainsi que l ont constaté S. Ridard et J.-L. Lambert [RL 95], la programmation d algorithmes dans un langage orienté-objet se fait parfois au détriment de l encapsulation. En effet, l utilisation de structures comme des conteneurs ne peut pas toujours se faire de manière séparée des objets. Ceci est particulièrement flagrant pour la programmation de structures qui évoluent de manière incrémentale par rapport aux caractéristiques des objets. Admettons, par exemple, que l on stocke dans un arbre, des objets triés suivant la valeur d un de leurs champs. Chaque fois qu un objet voit son champ bouger, on voudrait mettre à jour l arbre. Ceci implique que l on soit capable de retrouver la place de l objet dans l arbre. Ceci ne peut se faire (si on ne veut pas de surcoût en complexité) qu en rajoutant directement sur l objet des champs liés à l arbre (par exemple, les père et les fils). Une solution pour enrichir les structures pourrait pourrait consister à utiliser un modèle à héritage multiple avec des classes contenant des structures purement algorithmiques (ou des interfaces, en Java). Mais ce modèle n est pas suffisant (il suffit d imaginer le cas d objets appartenant à deux arbres triés, suivant la valeur de deux de leurs champs). L encapsulation est donc brisée puisque la structure initiale de l objet, provenant de la phase de modélisation du problème a été poluée par des informations supplémentaires, dont la fonction est uniquement algorithmique. Pour palier à ce problème, et permettre une description modulaire des algorithmes d optimisation où apparaissent séparément la modélisation du problème et la modélisation des objets algorithmiques, nous avons proposé d enrichir le modèle objet par un opérateur de génération de code, appelé foncteur de classes, qui puisse enrichir une classe avec de nouveaux champs et de nouvelles méthodes. Cet opérateur permet ainsi de décrire de manière modulaire et paramétrique les extensions algorithmiques faites au modèle objet de départ [Lab 96]. Ce champ de recherche nouveau est encore ouvert, des solutions diverses ont été proposées (objets algorithmiques [RL 95], observer pattern [KB 96]) mais aucune n a encore été implémentée. 9
155 9.3 Description déclarative de comportements En quittant le cadre pur de la programmation logique par contraintes pour considérer des algorithmes hybrides, nous avons élargi le champ des mécanismes algorithmiques que nous pouvions considérer, mais nous avons perdu l aspect déclaratif de la programmation. En effet, la PLC permet d encapsuler des mécanismes d inférence ou de consistance de sorte que l interaction entre l utilisateur et le système se retrouve très encadrée : l utilisateur écrit des formules qui doivent être vérifiées et le système renvoie des solutions qui vérifient ces contraintes. A partir du moment où l on se place dans le cadre général de la programmation impérative, toutes ces garanties sémantiques de correction disparaissent. On va donc chercher à encapsuler des comportements dans des modules dont on décrira l interface de manière déclarative (par des formules, et non par des instructions). Comme on veut décrire un ensemble de techniques algorithmiques différentes, on ne sait pas encore proposer un formalisme global élégant pour décrire tous ces mécanismes. On va passer en revue un certain nombre de propositions, qui soit existent déjà autour du système CLAIRE, soit sont des propositions de recherche Des contraintes sur les domaines finis (ECLAIR) De manière naturelle, le premier module que l on aimerait retrouver dans ce contexte est un module permettant d exprimer des contraintes sur les domaines finis, comme ce que l on trouve en CLP(FD) ou CHIP. Cependant, pour pouvoir utiliser ces mécanismes dans le cadre d un langage impératif, les mécanismes de contraintes doivent être mis à disposition sous forme de bibliothèque plutôt que de langage fermé. Nous avons ainsi créé une bibliothèque de contraintes sur les domaines finis, ECLAIR [LDJS 98], qui permet d exprimer des contraintes simples de manière élégante. On dispose de plusieurs classes pour représenter des variables, selon le mode de représentation qu on choisit pour le domaine de la variable (intervalle, ensemble énuméré, énumération stockée sous forme de vecteur de bits). Les contraintes arithmétiques s expriment avec la syntaxe naturelle, en utilisant le polymorphisme des opérateurs. On peut ainsi écrire de manière naturelle : X :: Var() Y :: Var() X - 2 * max(x,y) + 7 > 3 * Y Cette bibliothèque comporte aussi des contraintes globales dont les algorithmes de propagation sont un peu plus compliqués, par exemple = AllDifferent(X 1,..., X n ) impose que les variables X i prennent des valeurs deux à deux différentes, = Min(X 1,..., X n,) produit une variable de domaine dont la valeur doit égaler la valeur minimale prise par les variables X i (il existe une construction symétrique Max), = Element(Y, list(x 1,..., X n,)) impose à la variable Y de prendre la même valeur qu un des X i, = Occurrences(c, list(x 1,..., X n,)) produit une variable de domaine dont la valeur doit égaler le nombre de variables X i telles que X i a pour valeur c. = Within(a,b, list(x 1,..., X n,)) produit une variable de domaine dont la valeur doit égaler le nombre de variables X i telles que X i prend une valeur entre a et b. = un opérateur Booléen or pour exprimer la disjonction de deux contraintes (propagé en lookahead ) L application d ECLAIR aux programmes classiques d exemples de PLC permet de penser que l implémentation est compétitive avec les systèmes les plus répandus. 10
156 Problème Temps de résolution 16 reines 91 bk., 160 ms. Send + More = Money ABC + DEF = GHI Série magique (100) Systeq 10 Zèbre 3 bk., 20ms. 6 bk., 20 ms. 27 bk., 4.9s. 95 bk., 200ms. 2 bk., 10 ms. Ordonnancement : Pont 599 bk., 560 ms. Tableau 9-1 : Résolution de problèmes de contraintes sur les domaines finis avec ECLAIR (temps sur une SPARC Ultra 1) Aujourd'hui, la librairie ECLAIR comporte environ 2000 lignes de code CLAIRE, traduites en 5000 lignes de C++. Cette bibliothèque a deux grands intérêts : d'une part, il est fréquent de rencontrer dans des applications combinatoires tout un ensemble de contraintes annexes. Plutôt que de réécrire à chaque fois les propagateurs associés, on peut énoncer ces contraintes de manière déclarative, en utilisant ECLAIR. Ceci permet parfois de résoudre entièrement certaines applications de manière déclarative, et sinon, ceci permet de se concentrer sur les parties difficiles (la propagation de contraintes complexes, ou la stratégie de résolution), en énonçant de manière très compacte dans le code ces contraintes élémentaires. D'autre part, cette bibliothèque est très aisément extensible, puisqu'elle a été construite sur une architecture ouverte permettant l'ajout de nouvelles contraintes ou de nouveaux modes de stockage des domaines. Ceci permet, quand on développe un algorithme de propagation pour un problème combinatoire particulier, et que cet algorithme utilise le même type de propagation que celui d'eclair (visite en profondeur d'abord de l'arbre des déclenchements) de l'encapsuler sous forme de contrainte globale en ECLAIR. Ceci s'est produit pour le problème des polyominos, après avoir développé et validé un premier algorithme en CLAIRE (en programmant de manière impérative les règles d'inférence), on a pu ensuite mettre cet algorithme à disposition sous forme de nouvelles contraintes globales (ConsecutiveOccurrences et Increasing) et ainsi simplifier grandement la description de l'algorithme. Ces deux nouvelles contraintes sont maintenant vraiment réutilisables, puisqu'elles sont disponibles pour d'autres applications, et qu'elles sont capables de coopérer (propagation concurrente) avec toutes les autres contraintes de la librairie. Enfin, mentionnons que l'utilisation d'unebibliothèque de contraintes comme ECLAIR est pratique quand on cherche à exprimer des contraintes simples (en particulier, des contraintes arithmétiques) sur des objets individualisés. On va voir que pour des contraintes très structurées, impliquant beaucoup d objets et des algorithmes d inférence complexes, d autres formalismes sont disponibles Les règles logiques Un autre mécanisme, proche de celui des contraintes est celui des règles de production. L idée est la suivante : Une contrainte exprime des relations complexes entre différents objets par une seule formule décrivant une propriété de l état à atteindre. L utilisation de cette contrainte peut éventuellement faire appel à des algorithmes de propagation compliqués. Par opposition, les règles logiques utilisent le formalisme logique pour décrire non pas un état final (les propriétés de la solution), mais la dynamique de 11
157 l algorithme de propagation. Les règles permettent en effet de spécifier des schémas d inférence, indiquant quelles déductions peuvent être faites. On remarquera à cet égard que l évolution de la communauté de la PPC, en passant du formalisme CLP au formalisme CC a consisté à rajouter une construction (ask) permettant d exprimer des mécanismes complexes d inférences. Une règle logique comporte deux parties, une condition et une conclusion. La condition est une formule logique dans une algèbre bien définie et la conclusion peut éventuellement être une expression quelconque. Le comportement de la règle est le suivant : dès que la condition est avérée, la conclusion est évaluée. Les règles de production sont connues depuis OPS-5 [CW 88], toutes les différences entre les sytèmes résident dans l expressivité du langage logique considéré et la technologie (différentiation formelle, compilation) permettant de détecter que la condition est devenue vraie ou non. CLAIRE comporte un mécanisme de règles logiques, appelé MARIE (Moteur Algébrique de Résolution d Inférences Elémentaires), développé depuis 1987 [Ca 87]. Le langage logique est relativement riche puisqu on peut y exprimer des conditions complexes sur des objets avec des quantifications et des expressions ensemblistes. Celui-ci repose sur une algèbre relationnelle, ce qui fait que les règles sont plus adaptées à la programmation de contraintes génériques sur des ensembles d objets que sur des objets individualisés. Voici un exemple classique de règle, implémentant une fermeture transitive : rule((edge(x,y) exists(z, edge(x,z) & path(z,y))) => x.path :add y) Enfin, la technologie de compilation est particulièrement originale, puisque tous les chemins de déclenchement de la condition sont précompilés et qu il n y a plus à l exécution, aucun test d ordre syntaxique ou de pattern-matching. En ceci, la génération de code est radicalement plus évoluée que celle de l algorithme standard RETE [Fo 82]. Marie est ainsi capable de faire entre une centaine de millers et un million d inférences à la seconde, suivant la complexité de la règle. Pendant ce travail, nous avons utilisé à plusieurs reprises les règles logiques dans des situations où nous avions des inférences vraiment complexes à exprimer, comme dans le cas de l ordonnancement cumulatif. Dans ce cas, ces règles nous ont permis d exprimer de manière beaucoup plus simple un algorithme qu on aurait eu plus de mal à exprimer de manière impérative Invariants On vient de voir que les contraintes décrivent de manière déclarative une propriété de l état final de l algorithme (la solution à atteindre), alors que les règles de production permettent de spécifier des transitions entre des états intermédiaires (des déductions). Une troisième approche consiste à décrire des propriétés des états intermédiaires par des formules algébriques : il s agit de la formalisation par invariants, proposée dans le langage LOCALIZER [MvH 97]. L intérêt du formalisme d invariants est qu il permet de spécifier entièrement certaines structures en fonction d autres, ce qui simplifie grandement la description de l algorithme. Le langage de définition des invariants est similaire au langage logique de CLAIRE et offre les mêmes constructions ensemblistes. Ce formalisme est très élégant pour spécifier l évolution de structures qui évoluent de manière non-monotone au cours des transformations de l algorithme (par exemple, on pourrait imaginer définir l ensemble des intervalles de tâches par un invariant). Par exemple, on peut définir des invariants de la manière suivante en LOCALIZER (v est défini par une somme, C est un ensemble défini par sélection et D correspond à la relation inverse du tableau a) : 12
158 v = sum(i in S) a[i] C = {i in S a[i] = 0} D[i in I] = {j in S a[j] = i} On remarquera que d'autres formalismes d'invariants ont été proposés, par exemple pour la maintenance de structures de données complexes [KS 93] Du point de vue de l implémentation, la difficulté de mise en oeuvre des invariants vient de ce qu il faut être capable de les mettre à jour rapidement, après chaque transformation du système. La présence d expressions ensemblistes pour définir des valeurs (numériques ou ensemblistes) suggère que certains de ces calculs pourraient être faits de manière incrémentale. On sait par exemple aisément maintenir de manière incrémentale la somme des valeurs d une liste, le cardinal d un ensemble, un sous ensemble défini de manière intentionelle par une condition, etc. Pour implémenter ces mécanismes, deux types de technologies sont disponibles (comme pour les règles), suivant qu on interprète les invariants à l exécution (à la RETE) ou qu on analyse entièrement leurs définitions au moment de la compilation et qu on génère un code spécialisé pour la maintenance des ces formules. La première solution a été choisie par Localizer, et a l avantage de permettre l utilisation de tels formalismes sous forme de bibliothèques (en C++, par exemple). L autre direction consisterait à effectuer la même démarche que pour le compilateur de règles Marie, avec une technologie de compilation enrichie. Ainsi, on pourrait vouloir caluler la variation d une expression, par rapport à l ancienne valeur, dès qu une relation ou qu un objet intervenant dans l expression change. Notons enfin que le invariants pourraient être implémentés comme des objets algorithmiques [RL 95], positionnant des méthodes et des réflexes sur des données Propositions d autres formalismes On peut aussi imaginer fournir d autres langages à l utilisateur pour spécifier avec autant d abstraction et d élégance que possible l évolution des objets au cours de l algorithme. Une possibilité consisterait par exemple à pouvoir exprimer directement des contraintes topologiques complexes sur des relations, (c est à dire sur des graphes quand il s agit de relations binaires). Ce formalisme était offert dans le langage ALICE [Lau 78]. On pouvait ainsi exprimer des contraintes d inclusion de relations, forcer une relation à être une bijection, etc. (des contraintes de ce type ont aussi été offertes dans LIFE [AKP 90]). On pourrait ainsi imaginer offrir une classe riche de contraintes sur des graphes. Ce serait par exemple, la manière la plus élégante de spécifier un problème de voyageur de commerce, en disant que la relation cherchée doit être une sous-relation de la relation d adjacence de départ et qu elle doit former un cycle. La définition d une algèbre de contraintes sur les graphes dans laquelle il soit possible d exprimer les structures combinatoires classiques (flot, couplage, clique, ensemble indépendant, plongement, graphe de distance, forêt couvrante, etc. ) et pour laquelle on soit capable de faire une propagation efficace est un domaine de recherche entièrement ouvert. Une autre direction intéressante pour la spécification d algorithmes d optimisation avec un haut niveau d abstraction consisterait à donner un statut particulier aux opérations de sauvegarde et de restauration de solutions. En effet de nombreux algorithmes utilisent ces opérations (généralement implémentées de manière impérative au moyen de variables globales ou de fichiers). Cette description de la mémoire du passé de l algorithme par des variables globales est éventuellement acceptable tant qu elle n est utilisée que pour implémenter des mécanismes de retours en arrière, mais elle devient peu satisfaisante lorsque ces structures mémorisées sont utilisées pour rétablir des solutions partielles. Ces cas peuvent se présenter par exemple = en optimisation locale, quand on veut décrire le parcours complet d un voisinage d une solution comme une recherche globale que l on effectue par séparation et évaluation [Pe 97]. Dans ce cas, on 13
159 peut être amené à décrire des règles de production qui restaurent des morceaux de la solution précédente en fonction de la partie du voisinage que l on visite. = en optimisation locale encore, quand on implémente des algorithmes de type «shuffle» ou «shifting bottleneck», on est amené à garder successivement des fragments de solution. = dans tous les algorithmes combinant plusieurs solutions entre elles (par exemple, les algorithmes génétiques), on est amené à choisir de l information dans diverses solutions que l on a conservé. Toutes ces opérations sont pour l instant décrites de manière impérative. On pourrait proposer un cadre sémantique propre qui permette d aller lire de l information dans une solution passée. Ceci demanderait que les solutions aient un statut spécial dans le langage (peut être celui de monde ou d espace de calcul [SS 94], peut être quelque chose de différent) Les possibilités d implémentation sont nombreuses. Par exemple, on pourrait imaginer avoir une opération recall qui aille copier la valeur d une donnée à partir d une solution. recall(<solution>, <data>) On pourrait aussi éventuellement considérer une généralisation du mécanisme ask de CC (ce mécanisme permet de déclencher une action dès qu'on est sûr qu'une contrainte est vérifiée) qui soit indicée par un espace de calcul ask(<space>, <constraint>) => tell(<space>, <constraint>) Il s agit aujourd hui, de notre point de vue, d une direction de recherche totalement ouverte. 9.4 Contrôle Le quatrième point qui nous paraît important est celui de l expression du flot de contrôle dans l algorithme global. En effet, si l on s intéresse à la mise en œuvre d algorithmes hybrides mettant en jeu de nombreux composants algorithmiques, chaque morceau se prêtera naturellement à un style de programmation particulier, et l ensemble risque d être hétérogène et difficile à suivre. En effet, il nous semble que parmi les principaux paradigmes de programmation, la programmation fonctionnelle aura plutôt tendance à être utilisée pour la decription du fonctionnement de structures de données en utilisant l ordre supérieur pour décrire des procédures de manière paramétrique, la programmation logique aura tendance à être utilisée pour les algorithmes de recherche globale, en utilisant la possibilité de spécifier un algorithme de recherche par un but logique à atteindre. Enfin la programmation impérative aura tendance à être utilisée partout ailleurs, en particulier pour tous les algorithmes de recherche locale fonctionnant de manière itérative. Même si chaque style de programmation est bien adapté à la partie de l algorithme qu il exprime, l utilisation jointe de ces styles de programmation rend peu lisible le fonctionnement global de l algorithme. En effet, l utilisation de l ordre supérieur en programmation fonctionnelle, qui permet de composer ou spécialiser des algorithmes se fait souvent au détriment de la lisibilité (cela amène à définir des opérateurs très abstraits dont l utilisateur perd l intuition), et l utilisation du mécanisme de recherche implicite dans la programmation logique conduit parfois à des algorithmes dont l exécution peut être difficile à suivre. C est pourquoi il nous est apparu important de pouvoir décrire dans un formalisme unique le comportement d un algorithme hybride complexe. Les chapitre suivants y sont consacrés. 9.5 Environnement Le dernier point, même s il ne porte pas exclusivement sur la définition d un langage est néanmoins tout aussi important que les précédents. Le développement d algorithmes est une tâche de programmation 14
160 complexe et souvent expérimentale. De nombreux progrès restent à faire pour offrir un environnement de développement qui permette la programmation d algorithmes complexes qui soient pertinents, bien adaptés aux problèmes et sûrs. L environnement de programmation de CLAIRE est réduit, mais il comporte un certain nombre d outils qui se sont avérés très utiles. Citons parmi eux : = la présence d un interprète pour pouvoir facilement interroger une solution ou tester une nouvelle idée. = la présence, dans le code, de commentaires étendus : on dénote ainsi une série d instructions qui ne sont rendues actives que sous certaines options de compilation ou d exécution du système. On a ainsi la possibilité d activer la vérification d assertions, l appel de méthodes espions, associées à une méthode, ou l impression de certains messages de trace. = l utilisation d un débogueur de haut niveau dont on puisse, par exemple, spécifier les méthodes d affichage des structures de données. Ceci dit, le champ de débogage d algorithmes complexes (en particulier d algorithmes de recherche) est encore très ouvert. Beaucoup reste encore à faire pour enrichir le débogueur d'instructions de haut niveau, non seulement sur les structures de données (ce qui existe déjà un peu en CLAIRE), mais aussi sur des propriétés globales du monde courant. Il pourrait être intéressant, par exemple, de savoir à partir de quel instant, l état du système devient incompatible avec une certaine formule logique, ou bien à partir de quel instant, un état devient inaccessible par un ensemble de transformations données, etc. Dans la même veine de sujets, la visualisation, le suivi et l explication d algorithmes (en particulier, d algorithmes de recherche) sont aujourd hui des sujets entièrement ouverts. Enfin, il est évident que toutes les bonnes habitudes de génie logiciel comme l archivage de versions successives, ou la mise en place de séries de tests automatiques doivent être en particulier appliquées aux logiciels d optimisation. 9.6 Conclusion Nous avons indiqué cinq directions qui nous semblent importantes pour le génie logiciel des algorithmes hybrides. Comme ce programme de recherches est très large, nous n allons aborder dans les chapitres suivants qu un seul de ces points : la description globale du contrôle de l algorithme. Nous nous sommes concentrés sur ce point en particulier pour deux raisons principales : = La première raison est de l'ordre de la modélisation. Un des problèmes importants rencontrés lors du développement d'algorithmes complexes réside dans l explication de ce que fait l algorithme (on retrouve ici, une motivation du travail de 1993 sur CECILE). Si l on songe à la difficulté d expliquer simplement, par oral, la suite des opérations faites par un algorithme hybride (comme les algorithmes présentés en première partie pour les problèmes de tournées), on peut imaginer le problème que pose la maintenance ou le debogage de tels algorithmes. La difficulté vient en premier lieu des mécanismes de retours arrière présents dans les algorithmes de recherche. Nous avons donc cherché à décrire facilement des algorithmes de recherche globale, locale, puis hybrides. Partant du principe que le code simple à comprendre est facile à maintenir, il nous a semblé que la compréhension globale de ces algorithmes était un sujet important. = La seconde raison vise à faciliter la démarche expérimentale de mise au point d'algorithmes. Un obstacle au développement d algorithmes hybrides est la difficulté que l on peut avoir à précisément suivre le comportement du système ainsi qu à équilibrer les différents composants algorithmiques [JLJ 97]. Or, avant d'obtenir un bon algorithme, on est généralement amené à considérer de multiples variations similaires, ce qui représente généralement un effort de programmation conséquent. Pour 15
161 permettre d'envisager simplement ces variations sans avoir à réécrire une partie importante du code de l'algorithme, il faut avoir une description simple et concise de chacune des composantes de l algorithme, ainsi qu'un certain nombre d'opérateurs permettant de combiner ces procédures. Ces deux objectifs nous ont amené à proposer le langage SaLSA, ( Specification Language for Search Algorithms ), que nous allons présenter dans les chapitres suivants. 16
162 10. Chapitre 10. Définition du langage SaLSA 10.1 Pourquoi et comment décrire le flot de contrôle Description globale d un algorithme hybride. Les procédures de résolution hybrides en optimisation combinatoire peuvent suivre des cheminements complexes, construisant la solution pas à pas, prenant certaines décisions, en remettant d autres en question. Il arrive ainsi que la procédure globale ne puisse se décrire ni comme une série de transformations à partir d'une solution initiale (recherche locale, marche aléatoire), ni comme l'agrégation de décisions élémentaires (la recherche globale est ainsi une construction monotone où l on raffine de proche en proche une solution approchée ou sub-optimale en une solution complète ou optimale). Dans le cas d algorithmes hybrides, on considérera des combinaisons de telles méthodes. Dans de tels cas, le schéma global de l algorithme est plus difficile à suivre, et l on souhaite disposer d un langage permettant de décrire de manière concise l algorithme global. Le but est similaire à celui atteint par l utilisation d organigrammes pour décrire des procédures de décision complexes. On veut pouvoir embrasser d un coup d oeil la stratégie globale de résolution. Cette comparaison suggère aussi l utilisation d une syntaxe graphique. D ailleurs, on a souvent recours à des schémas représentant des arbres d hypothèses quand on cherche à expliquer de manière informelle ce que fait un algorithme de recherche. Une syntaxe graphique du langage SaLSA a été définie. Plutôt que de la présenter de manière formelle, nous illustrerons de temps en temps certains algorithmes par leur écriture graphique. L'intérêt de cette présentation graphique réside surtout dans son aspect pédagogique, on peut imaginer qu'il serait plus aisé à un utilisateur peu familier de SaLSA de programmer des algorithmes par des expressions SaLSA grâce à un tel éditeur graphique, plutôt que sous forme de texte. Ceci permettrait sans doute de rendre plus accessibles les constructions abstraites qui vont être décrites Description déclarative d un algorithme de recherche. Quand on cherche à décrire un algorithme, on oppose souvent les descriptions en pseudo-code (un mélange de langage naturel et de langage de programmation impératif universel que l on trouve dans la littérature) et celles en véritable code source. La différence entre les deux vient généralement du fait que le programme exécutable doit spécifier un certain nombre de détails d implémentation qui ont été laissés dans l ombre par la description en pseudo-code. Pour le cas qui nous intéresse des algorithmes de recherche, se pose la même question : à quel niveau de détail veut on s arrêter? Quand considérera-t-on que l algorithme a été suffisamment spécifié? La réponse que nous prendrons est dictée par les descriptions d algorithmes de recherche en pseudo-code dans les livres d algorithmique. La plupart du temps, la seule chose qui soit décrite est le mécanisme de séparation d un problème en sous problèmes, et le mode d exploration de l arbre de recherche (en profondeur d abord, A*, à profondeurs successives, sur des processus distribués, etc. ) n est pas spécifié, pas plus que le mode de représentation des noeuds de l arbre (stockage historique des données sur une pile, association d un marqueur à chaque valeur, copie de données redondantes, etc. ). En suivant notre objectif qui est d offrir une description synthétique et élégante des algorithmes hybrides, on choisira de laisser ouvertes par défaut, dans la description de l algorithme, tout un ensemble de décisions d implémentation. On laissera ensuite le soin au compilateur de générer un code exécutable, adapté à l environnement dans lequel celui-ci sera exécuté (calcul distribué ou non, optimisation de la mémoire ou de la vitesse, etc. ). 1
163 Distribution et non-déterminisme. On attend notamment de la description d algorithmes qu elles soient facilement parallélisables par le compilateur. On va voir que cette exigence va nous amener a faire des descriptions non déterministes des algorithmes. Prenons l exemple de procédures par évaluation et séparation distribuées : les recherches dans des sousarbres sont en compétition pour trouver la prochaine solution. Pour éviter de visiter tout l'arbre, chacun des sous-arbres en compétition procède à l'évaluation d'une borne inférieure (dans un problème de minimisation), et compare cette borne à une borne supérieure partagée par tous les sous-arbres (la valeur de la meilleure solution trouvée jusqu à présent). Dès que la borne inférieure (locale) dépasse la borne inférieure (globale), l'exploration du sous-arbre est arrêtée. Suivant l affectation des ressources de calcul aux différents processus, certaines parties de l exploration peuvent être privilégiées par rapport à d autres d une exécution sur l autre. La réponse de l algorithme (la séquence des solutions retournées) n est donc pas déterministe puisqu elle dépend de l affectation des ressources de calcul (la valeur de l optimum, elle, reste la même d une exécution sur l autre). Les descriptions des algorithmes de recherche que nous proposerons resteront donc, par défaut, nondéterministes (même si nous offrirons la possibilité de limiter ce non-déterminisme) Décisions locales Un algorithme de recherche peut se définir par deux données : des sources d indéterminisme (un ensemble de décisions, parmi lesquelles une seule peut être prise à la fois) et un mécanisme de contrôle permettant d enchaîner les décisions, de revenir en arrière, de limiter l exploration, etc. Le premier élément est essentiellement local (par exemple, en Prolog, il correspond à la donnée d un ensemble de règles ayant même clause de tête) alors que les effets du second élément sont globaux (ils forment l ossature de l algorithme). Les formalismes proposés jusqu à présent permettent une certaine expressivité sur seulement un seul des deux aspects. Par exemple, les langages de modélisation en optimisation locale offrent peu de possibilité de décrire le comportement global de l algorithme (possibilités limitées de faire de l exploration et du retour arrière). En programmation logique, ces structures locales sont exprimées par des constructions syntaxiques, qui sont ensuite manipulées par la stratégie de résolution SLD : ce mécanisme est donc limité par le formalisme des règles pour l expression d un choix. Enfin, il n existe pas de mécanisme général pour décrire un algorithme hybride permettant de mélanger optimisation locale et globale. On prend ici le parti de réifier les choix indéterministes pour en permettre une manipulation explicite dans un langage comportant des constructeurs spécialisés pour la recherche arborescente Des choix non déterminés Les algorithmes de recherche contiennent ainsi des sources d indéterminisme dans leur description que l on peut toutes décrire sous la forme suivante. On considère un ensemble de décisions (ou hypothèses) exclusives d 1,..., d n. L exécution se partage ensuite entre n branches qui chacune appliquent une des décisions d i. Les choix pourraient ainsi facilement être comparés à des automates indéterministes, dans lesquels on considérerait plusieurs transitions possibles à partir d un état. = Pour un algorithme d optimisation locale, les états correspondent à des solutions, et le choix permet de décrire un voisinage de la solution courante. = Pour un algorithme de recherche globale, les états ne sont pas des solutions directement interprétables, ce ne sont pas des solutions au problème. Ainsi, pour un algorithme de P.P.C., les états représentent 2
164 des solutions partielles, où certaines variables ne sont pas encore instanciées; pour un algorithme de P.L.N.E., les états représentent des assignations de valeurs rationnelles -et non entières- aux variables. Dans ces algorithmes de recherche globale, les transitions permettent généralement de transformer l état courant en un état plus proche d une solution. Ainsi, en P.L.N.E., les transitions instancient une variable dont la valeur courante est rationnelle à une valeur entière, en P.P.C., une transition peut consister à choisir une des branches d une disjonction entre contraintes. Nous avons choisi de proposer un formalisme unique pour décrire ces points d indéterminisme dans les algorithmes de recherche locale et globale. Par rapport aux approches existantes, ceci va nous permettre de 1. Préciser les boucles globales de contrôle pour des algorithmes d optimisation locale (en particulier, introduire un peu d exploration globale). 2. Décrire de manière plus fine les mécanismes de branchement dans un algorithme d optimisation globale, et en particulier, pouvoir combiner différents mécanismes de branchement. 3. Combiner facilement optimisation locale et globale Décrire un arbre de recherche à partir de simples transitions. Dans les algorithmes de recherche, la structure d arbre la plus simple se retrouve dans les traces d exécution de la procédure : à certains moments, l algorithme est revenu sur ses pas, en défaisant ce qu il avait fait. La présentation que nous proposons va donc être faite uniquement en termes de transitions que l on fait et que l on défait (retours en arrière). Les présentations habituelles des algorithmes de recherche, dans la littérature, décrivent un arbre en partant des noeuds. Ainsi, qu il s agisse d une présentation en programmation par contraintes, où l arbre est décrit comme un ensemble d états dans un treillis d information, ou d une présentation d intelligence artificielle (comme, par exemple, celle de Judea Pearl [Pea 84]), où l arbre est décrit comme une structure de données où les noeuds sont des objets directement accessibles), les algorithmes de recherches sont généralement présentés comme des parcours dans un espace déjà structuré, celui des états (ou des noeuds). Ce point de vue devient limitant quand le contenu d un noeud n est plus simplement un ensemble de formules logiques (dès qu on sort de la programmation par contraintes pure pour décrire des algorithmes hybrides) ou quand on se place dans une implémentation non distribuée, où l on n a pas forcément un accès facile et immédiat à un noeud quelconque de l arbre de recherche (au contraire d Oz [SS 88] ou de Bob [LCR 95]). Pour garder un cadre d application le plus général possible, la présentation que nous proposons prend un point de vue différent en construisant des arbres de recherches à partir d opérateurs sur ces états (les transformations effectuées par les branches des choix), sans présupposer aucune structure sur ceux-ci. Ces deux points de vue pourraient sembler équivalents, mais sont en fait très différents. En effet, dans les présentations basées sur les états, la recherche peut être totalement distribuée puisque les sous-arbres sont indépendants les uns des autres. A moins d introduire de nouvelles sémantiques comme celle de la négation par élagage [Fa 95], on ne peut donc pas exprimer des algorithmes qui nécessitent la communication d information d une partie de l arbre à l autre, comme l optimisation par séparation et évaluation qui nécessite la communication de bornes, l apprentissage d échecs par stockage de no-good sets, ou l utilisation d une liste tabou pour décider de la validité de mouvements. Pour permettre cette communication entre différentes parties de l arbre de recherche, on va décrire les algorithmes de recherche par des opérateurs faisant évoluer un système composé d un ensemble de processus (qui représentera une frontière de l arbre de recherche) et d un environnement global servant au passage d informations entre les différents sous-arbres de l algorithme. 3
165 Bien que ce ne soit pas l objet de cette thèse, mentionnons ici que ce parti-pris de décrire les arbres de recherche en oubliant toute structure sur les états permet de reconstruire, à partir des seules transitions d un noeud vers ses fils, toute une structure mathématique très riche : catégories, topologie discrète, topos, monades. Ce travail a été entrepris par Éric Jacopin [JLJ 97] Définition d un choix Comme il a été dit plus haut, la brique de base pour décrire un algorithme de recherche sera le choix indéterministe, c est à dire un mécanisme considérant un ensemble de décisions mutuellement exclusives, et les appliquant une à une. En fait, on va considérer qu un choix peut avoir deux comportements possibles : suivant qu il génère un ensemble vide ou non de décisions. Dans le cas où cet ensemble de décisions sera vide, on dira qu on se trouve à un point fixe pour le choix (celui-ci est incapable de nous faire passer de l état courant à un état voisin); dans le cas général, le choix produira un voisinage (strict) de l état courant. Nous allons maintenant décrire le langage pour créer des choix. Avant de présenter la grammaire complète, on donne un petit exemple didactique d un choix faisant l affectation d une valeur à une variable de domaine. Dans cet exemple, nous remarquons que les choix sont des objets nommés (celui-ci en particulier s appelle labeling), qui sont décrits par un ensemble de transitions (moves), indiquées par des expressions contenant d éventuelles variables libres. Dans l'exemple qui suit, cette expression utilise la construction post(s,e) qui permet d'évaluer un événement e et de demander à un solveur S de réagir à cet événement (l'utilisation de cette construction est évoqué un peu plus loin). Les variables libres sont définies après le mot-clé with : certaines sont définies par un ensemble de valeurs possibles (par exemple, ici, v values(x) ), d autres sont définies de façon univoque en fonction des précédentes (par exemple, ici, x = some(...) ). La construction some(x in E P(x)) itère l'ensemble E et renvoie le premier élément x qui vérifie la condition Booléenne P, et renvoie la valeur spéciale unknown si aucun tel élément n'a été trouvé. labeling :: Choice( moves post(fd, x == v) with x = some(x in Variable unknown?(value,x)), v values(x) on failure post(fd, x <> v) ) Dans la grammaire qui suit, on a noté en gras les mots réservés du langage, et en italique les parties optionnelles. Les constructions alternatives sont notées grâce à une barre verticale ( < a b c > désigne une occurrence soit de a, soit de b, soit de c). Les point des choix sont définis par le mot-clé Choice. Ils peuvent éventuellement prendre des arguments en paramètres (comme des fonctions). Dans l exemple suivant, les paramètres sont désignés x 1,..., x i. <ChoiceName> (x 1:<type>,...,x i:<type>) :: Choice( <ChoiceDef(x 1,...,x i )> ) Un choix avec pour variables libres (x 1,..., x i ) est défini par le mot-clé moves, soit directement par la définition d un ensemble de transitions, soit par un branchement conditionnel de type case qui évalue une expression et suivant le type de la valeur retournée, choisit un ensemble de transitions ou un autre. = Dans le cas d une définition directe, les transitions peuvent être triées et/ou filtrées par une condition Booléenne. Le tri (par une expression indiquée après les mots-clés sorted by) permet de rendre le 4
166 choix déterministe ou aléatoire; la condition Booléenne (indiquée après les mots-clés such that) permet de vérifier à l'exécution qu'une branche est digne d'intérêt, par exemple, en vérifiant que l'évaluation d'une borne inférieure n'est pas au-dessus de la borne supérieure courante). = Dans le cas d une définition par un branchement conditionnel (case), l expression de branchement peut introduire de nouvelle variables x i+1,..., x j qui devront être définies à partir des variables x 1,..., x i. De plus, on remarque que l on peut préciser certaines transitions additionnelles en cas d échec dans une branche, après le mot-clé on failure. L idée est la suivante : quand un échec est détecté dans une branche, une expression peut être évaluée au niveau du noeud père pour informer le reste de l arbre de l échec, soit de manière globale, soit de manière locale au niveau des branches sœurs de la branche à la cause de l échec. Ce comportement sera illustré dans des exemples plus loin, mais on peut d ores et déjà constater que ce mécanisme permet de faire remonter de l information dans un calcul distribué ou bien qu il revient à spécialiser les transitions inverses (celles qui permettent de revenir à un état antérieur) dans un calcul séquentiel mono-processus. Notons en particulier que ces instructions, en tirant parti de l'échec dans une branche, changent l'état du nœud-père (celui dans lequel a été posé le point de choix) : l'état associé à un nœud n'est donc pas constant, contrairement aux présentations des algorithmes de recherche basées sur les nœuds [Pea 84]. <ChoiceDef(x 1,...,x i )> ::= < moves <MovesDef(x 1,...,x n )> such that <condition(x 1,...,x n)>> sorted by <order(x 1,...,x n)>> with <vdef({x 1,...,x i },x i+1,... x n )> on failure <Dexp(x 1,...,x n)> > <case <exp(x 1,...,x j )> ( <type> <ChoiceDef(x 1,...,x j )>,... <type> <ChoiceDef(x 1,...,x j )> ) with <vdef({x 1,...,x i},x i+1,... x j)> > until <exp(x 1,...,x i )> L ensemble des transitions peut être défini = soit par une expression dont les évaluations (après les différentes substitutions possibles pour les variables libres) effectuent les différentes transitions du choix, = soit par une énumération d expressions, séparées par le mot-clé or, chacune correspondant à une transition différente, = soit par une expression SaLSA représentant un algorithme de recherche formé par composition de divers choix. <MovesDef(x 1,...,x n )> ::= <Dexp(x 1,...,x n )> or <Dexp(x 1,...,x n )>... or <Dexp(x 1,...,x n )> <Dexp(x 1,...,x n )> from <SALSATerm> Nous avons mentionné plus haut que les transitions pouvaient être triées (soit suivant la valeur d'une expression, soit au hasard) ou filtrées par une condition Booléenne. La condition Booléenne est évaluée avant la transition, sauf pour les expressions comprenant le mot-clé delta qui dénote la variation d une quantité au cours de la transition 5
167 <condition(x 1,...,x n )> ::= <exp(x 1,...,x n )> delta(<exp>) <operation> <exp> <order(x 1,...,x n )> ::= <increasing decreasing> <exp(x 1,...,x n )> random La section pour définir les variables libres x i+1,..., x n en fonction des paramètres x 1,..., x i est décrite cidessous. On s autorise en particulier l utilisation d expressions ensemblistes telles que {x in E P(x)}, {f(x) x in E}, some(x in E P(x)), etc. <vdef({x 1,...,x i },x i+1,... x n )> ::= with x i+1 <=, > <exp>, x i+2 <=, > <exp(x 1 )>,... x n <=, > <exp(x 1,...,x n-1 )> La définition du choix peut se terminer par le mot clé until suivi d'une expression. Cette possibilité est utiliser pour préciser certaines conditions dans lesquelles on veut que le choix s'arrête de brancher. Cette possibilité sera utilisée en particulier pour préciser des conditions d'arrêts à des recherches. Enfin, nous avons dénoté les expressions décrivant les transitions par la syntaxe <Dexp>. Ces expressions sont en effet des expressions spéciales puisqu elles peuvent soit être évaluées de manière distribuée sur différents processus, soit être évaluées puis défaites d une branche à l autre (dans le cas d une exploration séquentielle des branches d un choix). On donnera dans le chapitre 12 une sémantique opérationnelle de SALSA, dans un calcul de processus distribués (le souci de distribution provenant essentiellement de la clareté de la sémantique abstraite, et non d'une réelle contrainte d'implémentation). Pour garder une description déclarative des algorithmes hybrides, une des directions de recherche mentionnée dans la partie 9.3 consiste à décrire des structures algorithmiques qui réagissent automatiquement à certains événements. On suppose que l on a encapsulé de tels mécanismes dans des objets appelés résolveurs. Ces résolveurs peuvent ainsi implémenter des contraintes sur différents domaines, des invariants, des relations structurelles, etc. On suppose aussi que ces résolveurs ont l interface suivante: on peut leur fournir un événement auquel on leur demande de réagir par la construction syntaxique post(<solver>, <update>). On offre la possibilité d utiliser n importe quelle expression pour décrire les transitions d un arbre de recherche, mais les expressions faisant appel à un résolveur sont préférées car elles permettent de garder un style de programmation déclaratif. <Dexp(x1,...,xn)> ::= post(<solver>, <exp(x1,...,xn)>) <exp(x1,...,xn)> Ainsi dans l'exemple introductif, l'expression post(fd, x == v) suppose que l'on ait un résolveur FD qui fasse de la propagation de contraintes sur les domaines finis et à qui l'on rajoute un contrainte d'égalté entre la variable x et la valeur v. Si l on considère une exécution séquentielle pour les algorithmes de recherche, un choix est une partie du programme qui génère un ensemble d étiquettes quand elle reçoit la main (chaque étiquette est une valuation possible des variables libres de la transition), évalue une certaine expression sur la première étiquette, passe la main à une autre partie du programme, attend qu on la lui rende pour passer à la seconde étiquette, etc. C est donc une description incomplète du programme puisqu on a besoin de spécifier la procédure à qui l on passe le contrôle dans chaque branche. Il s agit ainsi d un combinateur d ordre supérieur (on remarque qu il en va de même pour le formalisme proposé en Oz [SS 94]). Pour passer de la description des simple choix à celle d algorithmes complets, on va introduire dans la section suivante des opérateurs de composition pour les choix (en particulier, les deux opérateurs et ). 6
168 10.3 Composition des choix Le but de cette section est de proposer un certain nombre de constructeurs permettant d exprimer le type de contrôle que l on retrouve dans les descriptions d algorithmes hybrides. On s inspirera des paradigmes de programmation déjà utilisés en algorithmique : = de la programmation logique, on retiendra que l on veut pouvoir exprimer l équivalent de la conjonction de buts, qui se traduit par l imbrication de recherches arborescentes, = de la programmation fonctionnelle, on retiendra que l on veut pouvoir non seulement composer des choix entre eux, mais aussi avec des fonctions quelconques, = de la programmation impérative et des formalisations des algorithmes de recherche locale, on retiendra que l on veut pouvoir exprimer des boucles itératives. Par souci de simplicité, les mécanismes de composition seront ici décrits de manière informelle. Une sémantique opérationnelle rigoureuse en est donnée au chapitre Composants On va définir un calcul pour exprimer les algorithmes hybrides. Les éléments principaux de ce calcul seront les choix dont nous avons décrit les mécanismes de création plus haut. Les termes composites que nous obtiendront offriront une description simple et synthétique du flot de contrôle au cours de l algorithme. Cependant, on cherchera aussi à combiner les composants arborescents avec d autres procédures. Pour cela, on cherchera à entrelacer l exécution de fonctions (transitions normales) avec celle de choix. En outre, pour décrire des algorithmes qui soient finis (que le flot de contrôle revienne en arrière lors de l'exploration), on introduira des terminateurs. On en considérera deux : = le terminateur exit qui permet de garder l état courant et d arrêter les explorations d autres sous arbres, suspendues ou en cours. = le terminateur cont, qui permet de continuer une exploration quand on est arrivé à une solution. Les paragraphes qui suivent décrivent des mécanismes de composition impliquant ces trois types de composants, à savoir des choix, des fonctions et des terminateurs Composition par union Pour présenter ce premier mécanisme de composition de choix, on revient à la comparaison avec les automates, où l on peut considérer qu un choix construit, à partir d un état, un ensemble d états que l on appelle voisinage. Si l on dispose de deux choix C et D, on peut associer à chaque état, deux voisinages. On peut considérer que l union de ces deux voisinages forme un nouveau voisinage de l état courant. On peut ainsi définir le choix C D, induisant comme voisinage l union des voisinages associés à C et à D. On va considérer plusieurs formes d unions de voisinages, des unions de voisinages deux à deux, des unions d une famille de voisinages, des unions conditionnelles, des unions commutatives ou non Union inconditionnelle La forme la plus simple d union consiste à agréger deux choix C et D en un seul C D. Celui-ci se comporte de la manière suivante : partant d un état, on peut appliquer soit une transition du choix C soit une transition du choix D. Les transitions de C D seront terminées quand toutes celles de C ainsi que celles de D auront été épuisées. D un point de vue opérationnel, cette construction correspond à la visite en parallèle de C et D. 7
169 De manière similaire, si l on dispose d un voisinage défini de manière paramétrique, disons C(x), on peut définir l union d un ensemble quelconque de tels voisinages, indicée par une collection E. On note ce nouveau choix (x in E)(C(x)). Si la collection E est un ensemble (donc, une collection non ordonnée), les branches de tous les choix C(x) pourront être visitées dans un ordre quelconque. On pourra aussi préciser un ordre d itération sur E (par exemple, si E est une liste). Dans ce cas, si y apparaît avant z lors de l itération de E, toutes les branches de C(y) devront être visitées avant que l on ne commence à explorer celles de C(z). Remarquons que dans le cas d une union indicée par une liste (une collection ordonnée), on contraint l ordre de visite des branches d un choix composite. Dans le cadre d une exécution distribuée, ce type de constructions revient à introduire des mécanismes de synchronisation entre les processus associés aux différentes branches de l arbre de recherche Union conditionnelle On considère maintenant un autre mécanisme pour agréger deux choix. L idée est la suivante : on voudrait pouvoir systématiquement considérer le choix C, mais lui substituer, quand celui-ci est incapable de brancher (i.e. on dira que l'on se trouve dans un état point fixe pour C), un autre choix D. On note cette construction C D. Ceci est utile, par exemple, pour faire de la résolution globale hiérarchique (à partir d une solution à un sous-problème, chercher à compléter en une solution à un problème plus grand). Ou encore, dans un cadre d optimisation locale pour chercher, pour faire du hill-climbing, et changer de famille de voisinages, quand la première famille a mené à un optimum local. De manière générale, on utilisera l opérateur pour composer un choix (ou un terme composite formé de choix) avec d autres composants algorithmiques (choix, fonctions, terminateurs). Les compositions C D, C f on C exit sont donc des expressions valides. Noter qu ici encore, l opérateur introduit () est non-commutatif Composition par imbrication On va décrire le mécanisme permettant d imbriquer des recherches arborescentes. Ceci revient à préciser vers où va le flot de contrôle lors que l on se trouve dans une feuille d un choix (ou plus généralement d un arbre SaLSA quelconque). Imbriquer ainsi deux point des choix revient, en programmation logique, à chercher une solution à une conjonction de deux buts. Pour illustrer ce mécanisme de composition, plaçons nous dans un cadre simplifié où l on puisse désigner l ensemble des transitions effectuées par un choix par un ensemble constant (indépendant de l'environnement). Composer deux choix revient à considérer l ensembles des transitions formées d une transition élémentaire du premier choix puis d une transition élémentaire du second. Le choix produit induit alors comme ensemble de transitions le produit cartésien des ensembles de transitions des deux choix élémentaires. Remarquons, qu à moins de faire certaines suppositions (constance et commutativité des transitions) sur les choix en question, ce constructeur n est pas commutatif. L opérateur pour représenter cette composition est noté. C C représentera ainsi la composée du choix C avec le choix C : ce terme décrit un petit algorithme de recherche où l on considère deux niveaux de décisions : on commence par prendre une des décisions proposées par le choix C puis une des décisions proposées par le choix C. On pourra ainsi composer un choix avec lui même. On utilisera (par analogie avec l arithmétique entière) la notation C n pour désigner la composée itérée n fois d un choix avec lui même. On pourra ainsi exprimer des termes correspondant à des algorithmes de recherche de profondeur arbitraire. On peut aussi combiner les choix avec des fonctions. Par exemple, C n f désigne un algorithme de recherche qui 8
170 applique le schéma de branchement décrit par C sur n niveaux de profondeur, puis à partir de chacune des feuilles, évalue la fonction f. Cette loi de composition est associative mais non commutative, puisque les transitions dépendent de l état à partir duquel on les applique. On peut aussi composer indéfiniment un choix C avec lui-même, c est à dire construire un arbre de recherche entier avec le même type de décisions. Par analogie avec la fermeture transitive, on note ce terme C *. Généralement, du moins pour des algorithmes de recherche globale, cette construction est finie. En effet, arrive un moment ou le choix C génère un ensemble vide d étiquettes. On dit dans ce cas que l état courant est un point fixe du choix. On propose une visualisation graphique des algorithmes hybrides. On associe à chaque choix un triangle dont partent deux flèches, une par le bas pour désigner la composition (imbrication des choix) et une par le coté pour désigner la composition (ce qui se passe quand le choix a généré un ensemble vide d étiquettes). Comme pour les organigrammes, le flot de contrôle de l'algorithme suit ces flèches. Graphiquement, on représentera les choix par des triangles et on pourra ainsi, par composition, former des algorithmes de recherche. La figure 10-1 représente les termes C D, D * g et C n f. Notons que les diagrammes ainsi dessinés ne représentent pas directement des arbres de recherches, mais une version plus condensée de l'algorithme. En effet, le flot de contrôle passe de nombreuses fois sur une même flèches, puisque toutes les branches d'un point de choix sont représentées par la même flèche. C D g C Š n D f Figure 10-1 : visualisation de termes SaLSA comme de petits organigrammes Autres constructions Cette section décrit quelques autres constructeurs (autres que les mécanismes de composition, et ) disponibles pour former des expressions SaLSA Recherche en avant La première primitive permet de faire la visite d un arbre et de ne retenir que les meilleurs états auxquels celle-ci a conduit. Ce procédé s appelle de la recherche en avant (look-ahead). Soit f une fonction d arité 0, n un entier, r un réel entre 0.0 et 1.0, et T un terme SaLSA, alors l exécution du terme smallest(t,f,n,r) va commencer par dérouler l exécution du terme T, évaluer f() dans chacun des états finaux auxquels aura conduit le terme T et calculera le minimum f min de ces évaluations. Ensuite, le terme smallest(t,f,n,r) se conduira comme un choix décrivant le voisinage composé des états précédemment atteints tels que f() r f min, en ne gardant qu au plus n d entre eux, ceux ayant donné les plus petites valeurs de f(), et en les 9
171 explorant dans l ordre croissant pour la valeur des évaluation de f(). Une primitive symétrique, largest, est aussi disponible. La figure 10-2 représente de manière graphique une recherche en avant. On connecte à la sortie du triangle représentant le terme T un trapèze orienté vers le bas et contenant un symbole crescendo. Ce trapèze symbolise la collecte des états, leur tri par ordre croissant pour la valeur retournée par l'évaluation de h et l'application du filtre ne gardant que les meilleurs états. T 4 h, 95% Figure 10-2 : Recherche en avant, représentation du terme smallest(t,h,4,0.95) Conditions topologiques sur les arbres de recherche Deux possibilités sont offertes pour exprimer des conditions sur la forme de l arbre de recherche décrit. La première est une condition qui affecte les interactions de l arbre avec l extérieur, la seconde est une restriction intrinsèque. La première construction (opérateur / ) permet de limiter le nombre de sorties d un arbre de recherche. Par exemple, l exploration de C* / 4 sera comme celle de C* jusqu à ce que les quatre premiers états points fixes pour C soient atteints. Dès que ces quatre états auront été atteints, l exploration se terminera prématurément (remontée au noeud racine). La deuxième construction (opérateur! ) permet de limiter le nombre d échecs (backtracks) à l intérieur de l arbre. Ainsi, T! n se comportera comme T tant que le nombre d échecs à l intérieur de T n excèdera pas n. Dès que ce nombre atteint n, l exploration se termine prématurément. La dernière construction where permet de spécifier dynamiquement un sous ensemble d un arbre de recherche que l on souhaite explorer. Ainsi, dans le terme T where f, le constructeur where se limite aux successions de transitions de T telles que f soit toujours vraie dans tous les états traversés. Dès que l exploration de T conduit à un état où f() renvoie faux, un échec est notifié (et le contrôle remonte au nœud-père). Cet opérateur (where) est ainsi une généralisation des conditions apparaissant après le mot-clé such that, dans les définitions de choix, au cas d expressions SaLSA arbitrairement complexes Grammaire du langage SaLSA Pour récapituler cette présentation, voici la grammaire du langage SaLSA, dont les expressions peuvent être utilisées, soit à l intérieur de définitions de choix, soit directement dans des expressions du langage impératif, par le biais des primitives SOLVE, min ou max. 10
172 <ClaireExpression> ::= <ChoiceName> (x 1:<type>,...,x i:<type>) :: Choice(<ChoiceDef(x 1,...,x i )>) SOLVE(<SALSATerm>) <min max>(<salsaterm>, <function>) <ChoiceDef(x 1,...,x i )> ::= < moves <MovesDef(x 1,...,x n )> such that <condition(x 1,...,x n)>> sorted by <order(x 1,...,x n)>> with <vdef({x 1,...,x i },x i+1,... x n )> on failure <Dexp(x 1,...,x n)> > <case <exp(x 1,...,x j )> ( <type> <ChoiceDef(x 1,...,x j )>,... <type> <ChoiceDef(x 1,...,x j )> ) with <vdef({x 1,...,x i},x i+1,... x j)> > until <exp(x 1,...,x i )> <MovesDef(x 1,...,x n )> ::= <Dexp(x 1,...,x n )> or <Dexp(x 1,...,x n )>... or <Dexp(x 1,...,x n )> <Dexp(x 1,...,x n )> from <SALSATerm> <condition(x 1,...,x n )> ::= <exp(x 1,...,x n )> delta(<exp>) <operator> <exp> <order(x 1,...,x n )> ::= <increasing decreasing> <exp(x 1,...,x n )> random <vdef({x 1...x i },x i+1...x n )> ::= with x i+1 <=, > <exp(x 1...x i )>,..., x n <=, > <exp(x 1...x n-1 )> <SALSATerm> ::= <SimpleTerm> <SimpleTerm> <SALSATerm> <SimpleTerm> <SALSATerm> <SimpleTerm> <SALSATerm> <SALSATerm> <SALSATerm> / <integer> <SALSATerm>! <integer> <SALSATerm> until <function> <SALSATerm> where <function> <terminator> <SimpleTerm> ::= <ChoiceName> (<ChoiceName> <ChoiceName>) (x in <exp>)<choicename(x)> <ChoiceName> <integer> <ChoiceName> * <smallest largest>(<salsaterm>,<function>,<integer>,<percentage>) <SimpleTerm> <function> <SimpleTerm> <function> <terminator> ::= cont exit 11
173 11. Chapitre 11. Utilisation de SaLSA pour résoudre des problèmes combinatoires Nous avons présenté brièvement dans le chapitre précédent le langage SaLSA pour décrire des algorithmes de recherche. Cette présentation est restée relativement informelle, mais abstraite. Nous allons maintenant revenir à la motivation pour la conception du langage, à savoir la résolution de problèmes d optimisation combinatoire. Nous présenterons une série d exemples d algorithmes d optimisation combinatoire impliquant une recherche (locale, globale ou hybride), et nous verrons comment ils peuvent être décrits en SaLSA. Nous allons présenter une dizaine d exemples. Leur but est de nous permettre d évaluer l utilisation de SaLSA par rapport au but visé : la description élégante d algorithmes hybrides. Nous espérons que le lecteur trouvera les descriptions en SaLSA lisibles et plus compréhensibles qu un code impératif habituel. = On commencera par illustrer la description d algorithmes de recherche globale en programmation par contraintes (exemple 1), = Ensuite on décrira des paradigmes de recherche locale (descente, recherche tabou et recuit simulé), = On décrira ensuite deux procédures heuristiques de construction directe. Sur la première (exemple 3), on illustrera l interaction entre recherche locale et construction incrémentale, sur le seconde, on illustrera l utilisation d'une forme de recherche limitée LDS (Limited Discrepancy Search) pour rendre cette heuristique plus robuste (exemple 4) = On décrira l utilisation de recherche en avant à profondeur limitée pour implémenter des mécanismes de propagation dans un cadre de recherche globale (exemple 5) = Dans les exemples 6 et 9, on décrira un mode de coopération entre recherche locale et globale, dans lequel chaque mouvement d optimisation locale est obtenue comme solution d une recherche globale. = Dans l exemple 7, on montrera comment les opérateurs min et max peuvent être utilisés pour représenter des stratégies dans un jeu à deux joueurs. = Enfin, l exemple 8 illustrera l utilisation d algorithmes de recherche sur des aspects différents de la résolution de problèmes, puisqu on ne cherchera plus dans l espace des solutions ni dans l espace des opérateurs à appliquer sur des solutions partielles, mais dans l espace des formalisations du problème (expression de contraintes redondantes) Stratégies de recherche globale pour un résolveur de contraintes sur les domaines finis. Comme premier exemple, on cherche à exprimer des algorithmes de recherche globale dans un système offrant des contraintes sur les domaines finis. Tous ces examples supposent que l'on a un moteur de propagation de contraintes sur les domaines finis, que l'on notera FD. Les procédures que nous allons décrire sont très proches de celles effectivement implémentées dans ECLAIR [LSDJ 98] et programmées en SaLSA. Pour les variables de domaines finis, on peut par exemple décrire trois mécanismes de branchement : = On sélectionne une variable non instanciée et on essaye récursivement toutes les affectations par une valeur de son domaine. Ceci peut être décrit par le choix L : 1
174 L :: Choice( moves post(fd, x == v) with x = some(x in Variable unknown?(value,x)), v domain(x) ) = Pour des variables comportant des domaines de grande taille, cette procédure est généralement remplacée par une procédure de réduction de domaine. Ceci peut être décrit par le choix R : R :: Choice( moves post(fd, x <= mid) or post(fd, x > mid) with x = some(x in Variable unknown?(value,x) & card(x) > 4), mid = (x.sup + x.inf) / 2 ) = Une dernière procédure utilise les disjonctions. En effet, on ne sait pas bien propager les disjonctions de contraintes, tant que l on ne sait pas laquelle des deux contraintes sera vérifiée par la solution. La procédure consiste ainsi à choisir une disjonction restée suspendue (c 1 or c 2 ), puis à essayer d imposer une des deux contraintes (c i ) de la disjonction. D :: Choice moves post(fd, d.c1) or post(fd, d.c2) with d = some(d in Disjunctions unknown?(side,d)) Avec tous ces choix, on peut maintenant décrire une stratégie générique de résolution globale, en appliquant chacun des choix en séquence (en prenant d abord toutes les décisions de type D, puis toutes celles de type R, puis enfin celles de type L) : SOLVE(D* R* L* exit) On peut aussi combiner ces trois procédures de choix de manière plus fine en les regroupant en un seul choix qui sélectionne dynamiquement le schéma de branchement (D, R ou L). Ainsi, le choix composite (Mix) prend une décision suivant le choix L tant qu il existe une variable non affectée avec au plus 5 valeurs dans son domaine, une décision suivant D s il existe une disjonction suspendue et pas de variable non affectée avec moins de 9 valeurs dans son domaine, et une décision suivant R sinon. Ce processus de décision est décrit par une instruction case qui teste le type de la paire d'objets (d,mincard) : Mix :: Choice case (d, mincard) ( tuple(any, (2.. 5)) moves post(fd, x == v) with v domain(x), tuple(disjunction, (9.. MaxInt) U {unknown}) moves post(fd, d.c1) or post(fd, d.c2), tuple(disjunction, (5.. 8)) U tuple({unknown}, (5.. MaxInt)) moves post(fd, x0 <= mid) or post(fd, x0 > mid) with mid = (x.sup + x.inf) / 2 ) with mincard = Min({Size(domain(x)) x in {x in Variable unknown?(value,x)}}) x = some(x in Variable Size(domain(x)) = mincard) d = some(d in Disjunction unknown?(side,d)) On peut aussi vouloir chercher à décrire des stratégies d optimisation par séparation et évaluation. On va voir que l algorithme précédent peut être facilement adapté au cadre de l optimisation. Pour cela, on dénote par OBJ la variable de domaine représentant la fonction objective à minimiser, et par UB une variable globale contenant la meilleure (plus faible) valeur objective trouvée jusqu à présent, au cours de la recherche. On définit ensuite deux fonctions utilitaires : la première pour stocker la valeur de l objectif lorsqu on se trouve à une solution, et la seconde pour contraindre le système à trouver des solutions strictement meilleures que celles qui ont déjà été trouvées. 2
175 registerub() -> UB := OBJ.value enforceub() -> post(fd, OBJ < UB) L algorithme d optimisation (qu il est impossible d exprimer de manière propre en Prolog) s exprime alors simplement sous la forme suivante : SOLVE(Mix* where enforceub registerub cont) Il a en effet suffit d'enrichir la procédure précédente de deux manières : d'une part, on limite l'exploration de l'arbre aux états dans lesquels l'évaluation de la fonction enforceub ne lève pas d'exception; d'autre part, à chaque solution, on évalue la fonction registerub avant de poursuivre l'exploration. Supposons maintenant que la valeur inférieure ne soit pas issue directement de la propagation des contraintes, mais plutôt d'un appel à un algorithme particulier (ce qui est très souvent le cas). On peut alors décrire l'algorithme d'optimisation par séparation et évaluation par le même terme que précédemment, mais en changeant la fonction enforceub de la manière à lever une exception (contradiction) dès que le calcul de borne inférieure renvoie un résultat trop élevé : enforceub() -> (if (computelowerbound() >= UB) contradiction!() ) L'algorithme d'optimisation par séparation et évaluation reste alors le même, à savoir : SOLVE(Mix* where enforceub registerub cont) 11.2 GSAT On s intéresse maintenant à un problème célèbre d optimisation combinatoire : 3-SAT. Ce problème est en effet un des problèmes NP-complets les plus célèbres. Soit un ensemble de variables à valeur Booléenne x 1,..., x n, le problème 3-SAT consiste à décider de la satisfiabilité de formules qui sont des conjonctions de disjonctions d au plus trois littéraux (un littéral est une atome x i ou sa négation). Un algorithme célèbre pour résoudre ce problème est une marche aliéatoire appelée GSAT [SLM 92]. Le principe est le suivant. On part d une valuation Booléenne quelconque sur les variables x i. Généralement, cette valuation viole un certain nombre de contraintes. Puis, on cherche à réparer la solution en changeant des valeurs une à une. On cherche à chaque fois à faire le changement de valeur qui diminuera le plus le nombre de clauses violées. On améliore ainsi la solution de départ par une suite de transformations (que l on borne par une constante MaxIter), et si à la fin de cette marche, on n a pas abouti à une solution satisfiable, on recommence. L algorithme parcours ainsi au plus MaxTries marches aléatoires (ces marches sont des processus de descente). Cet algorithme peut se décrire de manière très simple en termes de choix. On commence par définir : penalty() : integer -> card({c in Clause unsatisfied(c)}) Flip :: Choice( moves x.value := not(x.value) with x = some(x in Litteral exists(c in Clause involves(c,x) & unsatisfied(c)) such that delta(penalty()) < 0) On peut donc spécifier cette marche aléatoire de manière très simple par l expression suivante. SOLVE(smallest(Flip, penalty, 1, 1.0) MaxIter exit) Cependant, tel qu il est décrit, cet algorithme a peu de chances d être compilé de manière aussi efficace que des implémentations spécialisées. En effet, de nombreux calculs peuvent être faits de manière incrémentale, en maintenant certaines structures intermédiaires. Pour cela, on peut faire à un petit 3
176 résolveur, maintenant de manière incrémentale l ensemble des variables candidates à être changées de valeur, le coût attendu de tels changements, etc... (on trouvera une présentation détaillée de ces structures dans [MvH 97]). Si l on appelle SAT ce résolveur responsable du maintient des invariants, on peut alors réécrire de la manière suivante le choix (on remarque que le gain attendu d une transformation est évalué a priori puisque l'on utilise le mot-clé such that) : Flip2 :: Choice( moves post(sat, flip(x)) with x in LitteralsToBeFlipped sorted by random such that expected_gain(x) > 0 On définit un autre point de choix qui génère aléatoirement un nombre maximal (MaxTries) de solutions de départ (des affectations de valeurs aux littéraux), mais s'arrête éventuellement en cours de route, au cas où l'on serait arrivé à une situation réalisable (quand l'évaluation de penalty() retourne 0). On peut définir ce point de choix de la manière suivante. Init :: Choice( moves for x in Litteral x.value := random(boolean), post(sat, init_invariants()) with i in (1.. MaxTries) until penalty() = 0 L algorithme complet peut alors se décrire de la manière suivante SOLVE(((Init (Flip2 MaxIter / 1)) cont) exit) On propose dans le tableau 11-1 quelques expériences faites avec cet algorithme, sur des problèmes classiques (on utilise les mêmes valeurs pour MaxIter que celles prises dans [MvH 97]). MaxIter MaxTries temps pb1 (301 clauses) s. pb2 (645 clauses) s. pb3 (860 clauses) s. Tableau 11-1 : algorithmes en SaLSA pour GSAT Ces chiffres sont donnés ici à titre illustratif, pour montrer que le langage SaLSA peut être proposé avec un générateur de code efficace. Les temps d'exécution (sur une Ultra Sparc 1) pourraient être améliorés si l'on optimisait le générateur de code pour de tels algorithmes d'optimisation locale qui ne font aucun retour arrière (dans le code généré, tous les mouvements locaux sont effectués de manière à être défaisables). Une possibilité pour changer la boucle de contrôle consisterait à remplacer la phase de descente en recherche tabou. On peut par exemple refuser de changer trop souvent un atome en son opposé, en mémorisant dans une liste tabou (implémentant une file FIFO) les dernières variables dont la valeur a changée. On peut cependant accepter les mouvements tabous qui améliorent franchement la fonction objectif (ce mécanisme est généralement appelé critère d aspiration ), et a contrario, refuser ceux des 4
177 mouvements qui dégradent trop la valeur objective. Ce type de contrôle peut facilement être implémenté en modifiant la description du choix de la manière suivante. Flip3 :: Choice( moves post(sat, flip(x)), enqueue(tabu, x) with x in LitteralsToBeFlipped) such that (expected_gain(x) > delta (not(x tabu) & expected_gain(x) > -(epsilon))) On aurait pu tout aussi bien considérer une boucle de contrôle de recuit simulé plutôt qu'une recherche tabou, en considérant le choix suivant, où T représente le paramètre habituel de température et ou l évaluation de random() retourne un flotant tiré aléatoirement dans l intervalle ( ). Flip4 :: Choice( moves post(sat, flip(x)), T := T * with x in LitteralsToBeFlipped) such that (expected_gain(x) > 0 random() < exp(expected_gain(x) / (gain * T))) 11.3 Algorithmes d insertion pour des problèmes de tournées On s intéresse au problème des tournées, présenté au chapitre 7. Rappelons brièvement que le problème est défini par un ensemble de tâches à affecter à un ensemble d employés. Chaque tâche a une situation géographique donnée et le but est de produire un plan et une route par employé tout en minimisant la somme des déplacements des employés. Parmi les algorithmes classiques qui ont été mentionnés, on considère les algorithmes d insertion dont le principe général est le suivant : on sélectionne les tâches une à une (suivant un ordre de priorité qui reste à définir), on les affecte à un employé, puis on les insère dans sa route, entre deux autres tâches déjà qui lui ont déjà été affectées. Insert(t:Task, r:route) :: Choice( moves link(x,t,r), link(t,y,r) with (x,y) edges(r) ) Insert(t:Task) :: Choice( moves from r Routes (smallest(insert(t,r),length(r),1,1.0)) ) OneInsert :: Choice( moves from smallest(insert(t), TotalLength(), 1, 1.0) with t = some(t in Tasks unaffected(t)) ) Une heuristique simple choisit une tâche et une route, essaye toutes les insertions possibles et effectue celle qui a le moindre coût. SOLVE((OneInsert / 1)* exit ) Cet algorithme peut être raffiné en y ajoutant de l optimisation locale incrémentale. L idée est de réoptimiser après chaque insertion (on peut ainsi espérer corriger les erreurs résultant d insertions malheureuses et ré-optimiser la séquence des tâches à l intérieur d une route). Cependant, lors de l exploration à l avance des branches (les insertions prospectives pour estimer le coût), on ne peut pas se permettre d effectuer des calculs trop coûteux, alors qu au moment de l insertion définitive, on peut se 5
178 permettre une phase plus conséquente d optimisation locale. Définissons donc deux procédures d optimisation locale par les choix suivants. TwoOpt(t:Task) :: Choice( moves link(x,u,r), link(y,z,r) with r = route(t) ((x,y), (u,z)) SortedPairsOf(edges(r)) such that delta(length(r)) < 0 ) ThreeOpt(t:Task) :: Choice( moves link(x,u,r1), link(z,y,r2), link(t,w,r) with r = route(t), u = next(t), r1 RoutesCloseTo(r), r2 RoutesCloseTo(r), (x,y) edges(r1), (z,w) edges(r2) such that delta(totallength) < 0 ) Grâce à ces choix, on peut expliquer le cœur de l algorithme. En voici un rapide résumé en français qui devrait convaincre le lecteur que la description SaLSA est relativement lisible. On sélectionne une tâche non encore affectée, et, pour toutes les routes, on choisit la meilleure place d insertion de cette tâche. A partir de ces k propositions d insertion (k étant le nombre de route), on essaye pour chacune d entre elles de ré-optimiser la route qui vient de subir l insertion, par une passe de 2-opt limitée à n itérations. Enfin, parmi ces k propositions (améliorées) d insertion, on en choisit définitivement une et on ré-optimise le tout (la route qui vient de subir l insertion et les routes adjacentes) par une passe de 3-opt limitée à m itérations. Voici maintenant la description formelle exprimée en SaLSA. Le mécanisme d'optimisation locale incrémentale peut être décrit par le choix suivant : SmartInsert:: Choice( moves from smallest(insert(t) (TwoOpt(t)/1) n, TotalLength()) (ThreeOpt(t)/1) m with t = some(t in Tasks unaffected(t)) ) La procédure globale consiste à répéter cette procédure pour toutes les tâches, les unes après les autres. SOLVE((SmartInsert/1)* exit) On a ici complètement spécifié la boucle de contrôle d un algorithme hybride complexe, utilisant à la fois de la recherche locale et globale Heuristiques en ordonnancement. On décrit maintenant un ensemble d algorithmes spécialisés pour les problèmes d ordonnancement. Les algorithmes que l'on va décrire sont des méthodes globales incomplètes, basées sur la notion de construction incrémentale du plan (comme pour les algorithmes précédents de tournées). On sélectionne les tâches une à une et on les ordonnance au plus tôt, selon leurs contraintes de consommation de ressources. Si l'on suppose que l'on dispose d'un résolveur de contraintes spécialisé pour l'ordonnancement, appelé schedule, on peut décrire ce procédé par le choix SaLSA suivant. Ins :: Choice( moves post(schedule, t.start = t.start.inf) with t {t in Tasks available(t) & not(scheduled(t))} ) Un première heuristique naïve (H0) peut donc être décrite par l'expression suivante : SOLVE((Ins/1)* exit) Mais H0 est un algorithme glouton trop naïf car il choisit, à chaque point de choix, d'ordonnancer une tâche au plus tôt, sans vraiment choisir cette tâche (on prend la première que l'on trouve). Cet algorithme 6
179 peut être amélioré si l on décide de choisir la tâche à ordonnancer par une règle de priorité (par exemple, on pourra choisir la tâche la plus urgente, c'est à dire la tâche t dont la date de début au plus tôt t.date.inf est minimale). Il suffit alors de préciser un ordre de parcours pour l'itération des branches du choix. On obtient alors l'algorithme suivant (H1) : Ins2 :: Choice moves post(schedule,t.start = t.start.inf) with t {t in Tasks available(t)} sorted by increasing t.start.inf SOLVE((Ins2 / 1)* exit) De même que dans le paragraphe précédent, le critère de priorité peut être mesuré après l insertion plutôt qu a priori. Par exemple si on dispose d'une fonction bound qui calcule une borne inférieure de la date de fin de l'ordonnancement, on peut choisir d'ordonnancer la tâche qui fera augmenter le moins possible cette borne. On obtient alors l'algorithme (H2) suivant : SOLVE(smallest(Ins, bound, 1, 1.0)* exit) Comme toutes ces heuristiques H0, H1 et H2 sont des recherches, on peut leur ajouter un peu d exploration globale. Ceci nous donne une sorte de limited discrepancy search (LDS) [HG 95], ou l on choisit d explorer deux branches (insérer t ou insérer t ) lorsque les deux insertions n ont pas pu être départagées par la fonction d évaluation heuristique. Dans ce cas, plutôt que de départager les candidats ex-aequo de manière arbitraire, on préfère poser un choix et explorer les deux branches. Pour limiter la complexité globale, on se contente de poser des points de choix au début de l'arbre (proche de la racine). Cet algorithme (H3) se traduit par le terme suivant : SOLVE(smallest(Ins, bound, 2, 1.0) 6 smallest(ins, bound, 1, 1.0)* display exit) Pour avoir un meilleur contrôle sur la complexité de l algorithme, on peut chercher à limiter le nombre de fois où ce type de branchement est exploré. De plus, on peut vouloir adapter la quantité d exploration à la profondeur dans l arbre de recherche. Ceci est motivé par le fait que généralement, les décisions intervenant tôt dans la recherche (en haut de l arbre) sont déterminantes pour la solution et que les branches alternatives considérées à ce niveau entraînent une plus grande diversité dans les réponses de l algorithme que celles considérées plus bas dans l arbre. Voici un exemple d algorithme (H4) qui module de manière plus fine la quantité d exploration autorisée suivant la profondeur dans l'arbre : SOLVE( smallest(ins, bound, 3, 1.0) 4 smallest(ins, bound, 2, 1.0) 3 smallest(ins, bound, 1, 1.0)* exit) On propose dans le tableau 11-2 une comparaison des cinq algorithmes envisagés ci-dessus pour le problème d'ordonnancement et implémentés en SaLSA et ECLAIR. On utilise les mêmes problèmes test qu'au chapitre 3 sur l'ordonnancement disjonctif : MT06 est un jobshop de taille 6 6 [MT 63] et les problèmes MT10, ORB1, ORB2 et ORB3 sont des problèmes de jobshop de taille [MT 63],[AC 91]. On donne pour chaque problème la valeur de l'optimum, et pour chaque algorithme, la valeur de la solution retournée (que l'on cherche à minimiser), ainsi que le temps d'exécution sur une Sparc Ultra 1 (les temps d'exécution doivent être considérés comme indicatifs, car la fonction calculant la bornes inférieure n'a pas été optimisée). Comme cela était prévisible, les résultats s'améliorent et les 7
180 algorithmes deviennent plus lents quand on passe de H2 à H3, puis de H3 à H4. Le but du langage SaLSA est de permettre l'expérimentation aisée de telles variations. H0 H1 H2 H3 H4 MT06 (opt=55) 80 (0.01s.) 68 (0.02s.) 68 (0.05s.) 60 (0.6s.) 60 (1.9s.) MT10 (opt=930) 1489 (0.8s.) 1262 (0.1s.) 1262 (0.5s.) 1220 (39s.) 1205 (42s.) ORB1 (opt=1059) 1422 (0.08s.) 1456 (0.08s.) 1456 (0.4s.) 1344 (3.8s.) 1326 (9s.) ORB2 (opt=888) 1479 (0.07s.) 1157 (0.07s.) 1157 (0.3s.) 1038 (4.2s.) 1013 (7.8s.) ORB3 (opt=1005) 1472 (0.07s.) 1297 (0.09s.) 1297 (0.4s.) 1229 (9.6s.) 1189 (25s.) Tableau 11-2 : algorithmes d'ordonnancement disjonctif programmés avec Eclair et SaLSA 11.5 Méthodes complètes en ordonnancement et shaving On va maintenant considérer des méthodes exhaustives pour le problème d ordonnancement disjonctif. Plutôt que de sélectionner les tâches une par une et de les ordonnancer au plus tôt, on va considérer des paires de tâches et décider laquelle des deux s exécutera en premier. Cette technique, appelée edgefinding est bien adaptée à la recherche exhaustive car les branches des choix binaires sont exclusives (ce qui n était pas nécessairement le cas des algorithmes du paragraphe précédent). La procédure de recherche peut être décrite ainsi : EdgeFinding :: Choice( move post(schedule, t1 isbefore t2) or post(schedule, t2 isbefore t1) with (t1, t2) = some{pair in tuple(task, Task) common_resource(pair) & unordered(pair)} ) Une autre possibilité pour la recherche globale consisterait à affecter à chaque tâche une date d exécution parmi toutes les dates possibles. Cette approche est très inefficace comme méthode de résolution globale, car elle dépend de la granularité du temps (doit on énumérer des jours, des heures, des secondes, ), mais, elle est très intéressante quand on l utilise comme mécanisme de propagation (en faisant un peu de recherche en avant). Cette technique qui s appelle le shaving [MS 95] est basée sur l idée suivante (décrite au 3.3.3) : Soit t une tâche pouvant commencer entre les dates a et b. On va chercher à enlever de son domaine les valeurs impossibles (en rasant le domaine par les bords). Pour cela, on essaye d assigner comme date de début pour t la date a, et on propage. En cas d échec, a peut être enlevé du domaine, et on peut recommencer le processus avec a+1, etc. Ce processus est itéré jusqu à ce que l on arrive à un entier k tel que l affectation de la valeur a+k comme date de début pour t ne conduise plus à un échec. Cette procédure peut être décrite par le choix suivant : 8
181 Start(t:TASK) :: Choice( move post(schedule,t.start = v) with t = some(t in Task unknown(value, t.start)}, v (t.start.inf.. t.start.sup) sorted by increasing v on failure post(schedule,t.start!= v) ) La procédure de shaving consiste à faire une visite limitée de ce choix en s arrêtant dès qu une branche ne conduit pas à un échec de manière immédiate. On veut donc s arrêter à la première feuille du choix, puis revenir au noeud de départ (on veut défaire l affectation qui n a pas causée d échec) : SOLVE(Start(t) / 1 cont) On peut répéter ces réductions de domaine pour toutes les tâches. On cherchera cependant à examiner les tâches dans un ordre particulier. En effet, si l on a une relation de précédence entre t 1 et t 2, on peut réduire au moins d autant le domaine de t 1 que l on peut réduire celui de t 2. Pour éviter de faire du travail en double, il vaut donc mieux réduire le domaine de t 1 avant celui de t 2. On examinera donc les tâches dans un ordre compatible avec les précédences (par exemple suivant un tri topologique du graphe des précédences). SOLVE( t sortedtasksup (Start(t) / 1 cont)) De plus, on peut aussi bien réduire les bornes inférieures que les bornes supérieures des domaines. On peut ainsi considérer un choix symétrique End(t) qui fait l affectation des dates de fin possibles, en partant de la fin du domaine. Et un argument du même type que le précédent montre que dans le cas d une relation de précédence entre deux tâches t 1 et t 2, on a intérêt à appliquer la procédure de réduction à t 2 avant t 1. La procédure complète peut ainsi être décrite comme : SOLVE( t sortedtasksup (Start(t) / 1 cont) t sortedtasksdown (End(t) / 1 cont)) Enfin cette procédure peut être appliquée plusieurs fois de suite, jusqu à ce que l on arrive à un point fixe (plus aucune réduction n est effectuée lors d une passe). Pour cela, on peut créer un nouveau choix : Shave :: Choice( moves from t sortedtasksup (Start(t) / 1 cont) t sortedtasksdown (End(t) / 1 cont) until delta(sum(list{t.domain.card t in Task}) = 0 ) à partir duquel on peut décrire la procédure complète par SOLVE(Shave * exit) Cette procédure peut être encore enrichie si on la rend récursive. On cherche ainsi à détecter les échecs obtenus après deux niveaux d affectation (au lieu d un seul). Pour décrire cela, on peut remplacer le choix Shave par le choix suivant Shave2 9
182 Shave2 :: Choice( moves from ( t sortedtasksup ( Start(t) ( t sortedtasksup (Start(t)) t sortedtasksdown (End(t))) / 1 cont) t sortedtasksdown ( End(t) ( t sortedtasksup (Start(t)) t sortedtasksdown (End(t))) / 1 cont) ) until delta(sum(list{t.domain.card t in Task})) = 0 ) La procédure complète de shaving est alors décrite par le terme suivant : SOLVE(Shave2 * exit) 11.6 Optimisation locale et solutions partielles : «Shuffling» Dans ce paragraphe, on décrit un mécanisme d optimisation locale, où chaque mouvement est obtenu par une recherche globale limitée. Partant d une solution, on n en garde qu un fragment (une solution partielle) que l on essaye d'étendre en une solution complète meilleure que celle de départ, par une recherche globale limitée en nombre de noeuds. Ce qui justifie cette procédure est la remarque expérimentale suivante : quand il existe une solution, la recherche globale permet généralement de la trouver rapidement (avec très peu de retours-arrière). Ainsi, on peut donc espérer que quand on a gardé un bon fragment de la solution, et qu il existe une autre solution, meilleure, et comportant le même fragment, celle-ci sera trouvée rapidement par la recherche globale. La procédure consiste ainsi à essayer toute une famille de fragments pour explorer un voisinage varié autour d un solution. Ces algorithmes ont été développés en ordonnancement disjonctif (job-shop) dans [ABZ 88], [AC 91] et [CL 95a]. Pour décrire un tel algorithme, on a besoin de deux choix. Le premier choix, EdgeFinding permet de décrire une recherche globale basée sur des décisions d ordre relatif des tâches utilisant la même ressource, et a déjà été décrit dans la section précédente. Le second, Forget, est un choix qui nous propose, à partir d une solution, plusieurs fragments de solutions. Plus précisément, ce choix énumère toutes les ressources les plus critiques ({r in Resource tight(r)}) et propose comme solutions partielles des états où tout l'ordonnancement a été oublié sauf l'ordre relatif des tâches sur la ressource considérée. Ce choix a un comportement très différent des choix habituels de recherche globale, pour lesquels la décision prise dans chaque branche raffine la solution courante. Ici, le comportement est différent, puisqu on oublie de l information en descendant dans la branche. Ce mécanisme de gestion des solutions complètes et partielles peut être programmé en utilisant les mécanismes de sauvegarde de solutions et de lecture d'information dans ces sauvegardes, comme ceux évoqués au paragraphe Nous allons ainsi supposer que nous disposons d'un nouveau types d'objets dans lesquels on puisse stocker des solutions précedemment vues. On stockera ainsi la dernière solution dans une variable lastsolution. On suppose encore que l'on dispose d'une primitive save_state permettant de sauvergarder l'état courant dans une telle variable, et de réinitialiser l'état courant. Enfin, on suppose que ces états sauvegardés peuvent être interrogés par un opérateur ask qui permette d'aller tester la validité d'une contrainte dans une telle solution passée. On peut alors décrire de la sorte un solveur qui va d'une solution à une solution partielle en commençant par sauvegarder l'état courant dans une variable, puis restaure à partir de cette solution en mémoire, l'ordre d'exécution des tâches sur une ressource donnée, cette ressource étant choisie de manière indéterministe parmi les ressources les plus critiques. On peut retrouver cet ordre des tâches partageant une ressource commune en interrogeant la solution sauvegardée pour savoir parmi les paires de tâches t,t', celles qui vérifient la contrainte 10
183 t.date + t.duration t'.date (auquel cas, t a lieu avant t'), et poser ces contraintes dans l'état courant (en les postant au solveur schedule). Forget :: Choice( move (save_state(lastsolution), for t in r.users for t' in (r.users but t) ask(lastsolution, t.date + t.duration t'.date) => post(schedule, t.date + t.duration t'.date) ) with r {r in Resource tight(r)} ) On peut alors combiner ce solveur d'oubli (permettant de passer d'une solution complète à une ensemble de solutions partielles) avec le résolveur EdgeFinding (vu précédemment) qui ordonnance des paires de tâches partageant la même ressource, et qui permet fait une recherche globale de solution à partir d'un état queconque (en particulier, à partir d'une solution partielle). SOLVE(Forget / 3 (EdgeFinding*! 100) exit) Le terme écrit ci-dessus permet de passer d'une solution à une autre, ces deux solutions ayant un ordre d'exécution commun sur une ressource. Il s'agit en quelque sorte d'un mouvement d'optimisation locale. On peut alors décrire chaque itération de cette procédure d'optimisation locale par un choix (Shuffle) : Shuffle :: Choice( moves from (Forget / 3) (EdgeFinding*! 100) until delta(ub) 0 La procédure complète d'optimisation locale applique de tels mouvements tant qu'elle en trouve qui améliorent strictement la solution (stratégie de descente). On peut la décrire ainsi : SOLVE(Shuffle* exit) 11.7 Jeux d échecs Cet exemple décrit des stratégies pour jouer aux échecs. Bien que cet exemple sorte du cadre des problèmes d optimisation combinatoires que nous nous sommes fixés, il est intéressant car l apprentissage du jeu d échecs à des ordinateurs est un domaine qui a motivé le développement d algorithmes de recherche. On peut décrire une partie de la manière suivante : chaque joueur cherche à jouer le coup qui le mettra dans la meilleure situation possible. Supposons que le joueur blanc dispose d une fonction d évaluation f b (renvoyant de fortes valeurs pour des situations favorables) qui lui permette d évaluer une configuration du plateau après un coup du joueur noir, et que, symétriquement, Noir dispose d une fonction f n qui permette d évaluer une configuration après un coup de Blanc. Supposons enfin que l on a un choix MvtB (resp. MvtN) considérant, à partir d une situation donnée, tous les mouvements que peut faire blanc (resp. noir). Une recherche bâtie de manière alternée sur les choix MvtN et MvtB correspond à la simulation d une partie. Dans ce cadre, une stratégie à un coup pour Blanc peut ainsi être décrite par l expression SOLVE(largest(MvtB, f b, 1, 1.0) exit) On va voir que les constructions min et max vont permettre d exprimer des stratégies plus compliquées. Par exemple, si Blanc examine à l avance toutes les réponses possibles de Noir, sa stratégie peut être : 11
184 SOLVE(smallest(MvtB, max(mvtn, f n )) exit) Blanc peut aussi raisonner avec trois coups d avance, et ne considérer que les 5 meilleures répliques de Noir pour limiter sa recherche, ce qui peut être décrit par l expression suivante : SOLVE(largest(MvtB, min(largest(mvtn, f n, 5, 0.0), max(mvtb, f b ) )) exit) 11.8 Recherche de bonnes cliques maximales en ordonnancement cumulatif On s intéresse à un problème d ordonnancement cumulatif, dans lequel on va utiliser un algorithme de recherche non pas pour trouver une solution ou l améliorer, mais pour préciser la manière dont on va spécifier le problème. En effet, pour certains types de données (comme la série des problèmes KSD30 [KS 95], présentés au paragraphe 6.4.3), il est intéressant de rajouter à la modélisation du problème des contraintes redondantes d ordonnancement disjonctif. La raison en est la suivante : certaines paires de tâches peuvent être disjonctives (c est à dire que les tâches sont soit l une avant l autre, soit l une après l autre mais en aucun cas, ne peuvent être concomitantes), soit parce que la somme de leurs consommations de ressource excède la disponibilité totale de la ressource, soit à cause d une relation de précédence. Et lorsque le problème est très dense en disjonctions [BL 97], on a intérêt à les traiter de manière groupée, sous forme de contraintes disjonctives redondantes. Plus précisément, on cherche à connaître des cliques de disjonctions, c est à dire des ensembles de tâches telles que toute paire de tâches prises parmi cet ensemble soit disjonctive. Il existe de nombreuses disjonctions maximales, on ne peut donc pas créer une contrainte redondante par disjonction maximale (l algorithme de propagation deviendrait trop coûteux). On cherche donc de bonnes cliques, par exemple des cliques comportant beaucoup de tâches, ou bien des cliques maximisant la somme des durées de leurs tâches. La recherche de cliques peut se faire par un algorithme de recherche globale : on part d un ensemble vide auquel on rajoute une par une des tâches étant disjonctive avec toutes celles sélectionnées jusqu à présent. A chaque insertion, il peut exister plusieurs candidats. Ce mécanisme peut être décrit par le choix suivant. C :: Choice moves S :add t with t {t in Task forall(t in S, disjunctive(t,t )) } sorted by decreasing t.duration Si l'on veut générer au plus trois contraintes redondantes, à partir de cliques dont les poids soient à moins de 10% du poids de la clique de poids maximal, la procédure peut s écrire : SOLVE(largest(C*, totalduration, 3, 0.90) make_redundant_constraint cont) 11.9 La procédure d optimisation locale de Lin & Kernighan pour le TSP On cherche à décrire la procédure d optimisation locale pour le TSP, proposée par Lin et Kernighan [LK 73]. Cette procédure généralise les voisinages k-opt, qui consistent à remplacer k arêtes du tour par k nouvelles arêtes. L union des ces 2k arêtes forme un cycle alterné, dont la somme alternée des poids des arêtes (distance) représente le gain en longueur du mouvement. La procédure de Lin et Kernighan consiste à construire itérativement ce tour, a priori pour une valeur de k quelconque, grâce à certaines 12
185 observations sur les sommes partielles alternées des arêtes (pour une explication plus détaillée de l algorithme, on se reportera au paragraphe ). On décrit ci-dessous de manière détaillée comment cet algorithme peut être programmé grâce à SaLSA. On prend la place de décrire cet algorithme en détail car il s agit d un algorithme célèbre, complexe et toujours délicat à programmer (une preuve en est que la plupart des implémentations récentes de cette heuristique [Rei 94] considèrent des variantes plus simples à coder que la spécification originelle). On commence par indiquer les structures de données utilisées en les déclarant en deux blocs : le premier (dans le bloc Global(...) ) décrit les structures de données partagées par tous les processus (tous les nœuds de l'arbre de recherche). Ici, il s'agit de la matrice de distance d, utilisée pour calculer la longueur du tour, les domaines possible_pred qui donnent l'ensemble des voisins de chaque nœud d'où peut venir le tour, et la relation pred qui associe à chaque nœud le voisin d'où l'on vient dans le tour de référence. On décrit ensuite, dans le bloc Local(...), les structures de données locales, celles qui évolueront de manière différente dans chaque branche de l arbre de recherche. Ici, il s'agit simplement d'un liste de nœuds l, qui permettra de construire le cycle alterné : on remplacera l'arête entrante en l[i] pred[l[i]],l[i] par l'arête pred[l[i+1]],l[i]. On cherchera à augmenter cette liste jusqu'à obtenir un cycle. Pour décrire cet algorithme de manière déclarative, on suppose que l on a un résolveur, appelé LK, qui maintient à jour une donnée (gain) définie par une formule ensembliste et qui cause un échec dès que cette donnée devient négative ou nulle. On propose de définir ce solveur par un invariant et par un démon. Le mécanisme d'invariant correspond au mécanisme implémenté dans LOCALIZER [MvH 97] : ici, on maintient la somme pour i allant de 1 à n = length(l)-1, des valeurs de f(i) = d[pred[l[i]],l[i]] - d[l[i+1],l[i]]. Comme indiqué dans [MvH 97], un invariant défini de la sorte peut être maintenu à jour en temps constant à chaque modification de la relation pred. Le démon, quant à lui, déclenche un échec dès que la valeur de l'invariant gain devient négative. Un tel démon peut être programmé par exemple avec une règle de production de MARIE [Ca 87], le moteur d'inférence de CLAIRE. Un tel solveur correspond donc à des technologies d'inférence maîtrisées. Ce qui est sans doute plus inhabituel est le regroupement d'un ensemble de tels mécanismes d'inférence différents sous le nom de résolveur. On décrit ensuite deux choix pour animer ce solveur, un premier (Init) pour initier le cycle, et un second (Augment) pour augmenter ce cycle alterné. (Ces deux points de choix correspondent à une présentation un peu simplifiée de la procédure originelle, mais plus simple à spécifier). 13
186 // data in the global environment: Global( d[x:city, y:city] : integer pred[x:city] : City possible_pred[x:city] : set[city] ) // the distance matrix // the immediate predessor of a city // (used to store the tour to optimize) // the candidates to be the immediate predessor // data in the local environments (in each node of the tree) Local( l:list[city] := nil ) // The solver LK LK :: (invariant(dist = sum(d[pred[i],i], i in City)), invariant(gain = sum(d[pred[l[i]],l[i]] - d[l[i+1],l[i]], i in (1.. length(l)-1))), daemon((gain 0) => failure) ) // choice points Init :: Choice( moves post(lk, l := list(x)) with x {x in City exists(y in possible_pred[x] d[y,x] < d[pred[x],x]]}) Augment :: Choice( moves post(lk, l :add x) with x City until (length(l) > 1 & l[1] = l[length[l]]) ) L exploration de l'espace de recherche pour trouver un mouvement (telle qu elle est décrit dans l'article originel [LK 73], et non une de ses multiples variantes plus récentes) peut se décrire ainsi : SOLVE(largest(Init (Augment (Augment / 1)*), gain, 1)) et l'algorithme d optimisation locale, utilisant ces voisinages, noté (LK) peut être décrit ainsi : SOLVE(largest(Init (Augment (Augment / 1)*), gain, 1, 1.0)* exit) Pour illustration, on donne le code généré par ce dernier terme dans le chapitre suivant ( ). Remarquons que SaLSA permet d expérimenter facilement d autres algorithmes de recherche, à partir de ces descriptions. Par exemple, on peut retenir plus de mouvements dans chaque voisinage (ici, on en retient au plus trois), et vouloir faire de la recherche globale sur les enchainements possibles de ces mouvements. On définit ainsi l'algorithme (LK avec exploration) par l'expression : SOLVE(smallest( largest( Init (Augment / 2) 2 (Augment / 1)*), gain, 3, 0.95)*, dist, 1, 1.0), exit) On propose dans le tableau 11-3 une comparaison des variations sur la procédure de Lin et Kernighan envisagées ci-dessus et implémentées en SaLSA. On indique, pour 5 TSP à 50 nœuds générés aléatoirement, la valeur (la longueur du tour) retournée par une heuristique gloutonne, puis la valeur retournée et le temps d'exécution des deux algorithmes envisagés. Comme on pouvait s'y attendre, l'ajout de recheche globale ralentit l'algorithme et améliore la qualité des solutions retournées. 14
187 heuristique LK LK avec exploration pb s s. pb s s. pb s s. pb s s. pb s s. Tableau 11-3 : variations sur la méthode de Lin et Kernighan, implémentées en SaLSA. Mentionnons enfin que les résultats obtenus ici sont un peu pessimistes en temps d'exécution et en qualité de solution, par rapport à [LK 73]. L'implémentation testée est valable aussi bien dans le cas symétrique qu'asymétriques : pour cela, on a limité la recherche de mouvements à des mouvements ne changeant pas l'orientation de sous-chaînes, ce qui diminue sensiblement l'efficacité de la procédure sur les problèmes à distance symétrique. De plus, on n'a pas utilisé certaines règles de coupes permettant de limiter les itérations à chaque niveau de l'arbre. Une implémentation de cette procédure optimisée pour le cas symétrique, et faisant les calculs de manière complètement incrémentale, fournirait de meilleurs résultats. Enfin, la figure 11-1 représente graphiquement le flot de contrôle de ce dernier algorithme. Init Aug Š 2 2 Aug % gain, 1 dist, 100% Figure 11-1: Procédure de Lin & Kernighan enrichie par un peu de recherche globale 15
188 16
189 12. Chapitre 12. Sémantique opérationnelle et implémentation 12.1 Préliminaires et notations Le propos de ce chapitre est de définir l évaluation d expressions du langage SaLSA. On commencera par présenter formellement la sémantique opérationnelle d'un fragment représentatif du langage (pour garder une présentation simple, on se limitera aux constructeurs principaux, et indiquant comment les autres peuvent s'exprimer en fonction de ces opérations de base). On décrira ensuite succinctement l'implémentation d'un générateur de code (produisant du CLAIRE à partir de SaLSA) qui a été faite en accord avec cette sémantique opérationnelle. La sémantique opérationnelle sera présentée sous forme de règles permettant l'évaluation d'expressions par la succession d'une série de transitions. Les règles indiquent sous quelles conditions de telles transitions peuvent être appliquées. On adopte la présentation habituelle des règles (utilisée par exemple, en théorie de la démonstration ou en réécriture) : une barre horizontale sépare des expressions représentant des transitions. De telles règles indiquent que les transitions sous la barre horizontale sont licites si toutes les transitions au dessus de la barre le sont. Quand une règle ne contient rien au dessus de la barre, alors la transition sous la barre est toujours licite. Comme SaLSA n'est pas un langage de programmation complet, mais seulement un ensemble de primitives d'ordre supérieur, il faut utiliser SaLSA en liaison avec un autre langage de programmation (par exemple, CLAIRE). Nous décrirons les transitions de SaLSA à partir de l'évaluation des expressions du langage de programmation hôte (i.e. CLAIRE). On notera de la manière suivante le fait que l expression fermée (sans variables libres) exp retourne la valeur val. ( ξ, E): exp ( ξ',e' ): val La paire (ξ,e) représente l état du système lors de l évaluation (l ensemble d objets existants, les valeurs des variables globales, etc. ). On note que cet environnement est séparé en deux parties, ce qui n'est pas justifié par l'évaluateur du langage hôte. En revanche, on va introduire un calcul de processus distribués pour décrire le comportement de termes SaLSA, dans lequel chaque processus aura sa propre version d une partie des données, alors que d autres données seront partagées par tous les processus. On distingue ainsi la partie globale du système, un environnement ξ partagé par tous les processus (ceux-ci y ont accès en lecture et en écriture, ce qui permet la communication entre processus distincts), de la partie locale du système, un environnement E, distribuée sur chacun des processus (chacun en ayant sa propre version). Noter que lors de l évaluation, l'expression exp peut modifier, par des effets de bord, des données dans chacune des deux parties de l environnement. Pour que cette présentation reste simple, nous allons considérer que toutes les définitions de choix sont de la forme suivante : C :: Choice( moves g(x) with x f() such that k() on failure h(x) ) et on les notera alors Choice(f,g,h,k). En fait tous les choix peuvent se réécrire sous cette forme après un peu de réécriture : 1
190 = si le choix utilise plusieurs variables locales définies après le mot-clé with, celles-ci peuvent être regroupées en une seule, dont la valeur est un n-uplet. Il suffit d agréger toutes les définitions de ces variables en une seule fonction f, = on peut de même intégrer dans la fonction f les conditions qui apparaîtraient après le mot clé until, ainsi que l ordre d évaluation éventuellement précisé après les mots-clé sorted by. = les choix qui énumèrent directement leur transitions g 1 or g 2.. or g k peuvent être représentés aussi sous cette forme en considérant une fonction f() qui renvoie l ensemble de valeurs {1.. k} et une fonction g telle que g(i) appelle g i (). = etc.... On pose alors les conditions suivantes sur ces quatre fonctions f,g,h,k utilisées pour la définition du choix. On demande que les quatre fonctions f,g,h,k respectent les conditions sémantiques suivantes : 1. f est de type void set[x] list[x], sans effets de bord, 2. g est de type X {void, }, avec d éventuels effets de bord sur E, 3. h est de type X void, avec d éventuels effets de bord sur ξ et E, 4. k est de type void bool, sans effets de bord. où est une valeur spéciale du langage utilisée pour dénoter l échec (d un point de vue opérationnel, elle peut correspondre à la levée d un exception non attrapée). De même que l on s est ramené à une définition simplifiée et uniforme des choix, on va s arranger pour n avoir à considérer que des expressions composées relativement simples. On introduit un nouveau terminateur ε (que l on appelle le terminateur ouvert), tel que C ε = C et C ε = C. On expanse aussi toutes les puissances, en remplaçant C n par C C... C et C * par le terme infini C C C... Enfin, on reporte dans la définition des choix, les conditions de visite (après le mot-clé where), les limites en nombre de backtracks, etc. On peut alors réécrire tous les termes sous forme de termes binaires (éventuellement infinis) C <SALSATerm> <SALSATerm>, où C est soit un nom de choix, soit un terminateur, soit une construction composite smallest(<salsaterm>,f,n,r) ou <SALSATerm> / n. Maintenant que l'on a définit le langage restreint qui allait nous intéresser et que l'on a montré qu'il constituait un fragment suffisamment expressif pour être représentatif, on va décrire les règles de transitions pour l'évaluation d'expressions SaLSA. A cet effet, on introduit des processus distribués (grosso modo, il y aura un processus par noeud dans les arbres de recherche), et on définit les transitions d un système composé d un environnement global et d un ensemble de processus. Les processus seront des objets notés p : qui auront les champs suivants : { } R state: start,expand,wait,done, failed R Rfather : R p : R sons : set[(< object >, )] R env : set[< object >] R R R code:< SALSATerm > R Les processus ont ainsi un état (cet état vaut start lors de la création, puis évolue en prenant successivement les valeurs expand, wait puis done, à moins qu'il ne finisse en état failed). Les processus sont organisés en une structure arborescente (chaque processus a un père et un ensemble de fils), les arêtes de cet arbre sont étiquetées (puisque le champ son contient des paires associant un objet (une 2
191 valeur) à chaque processus-fils). Enfin, chaque processus contient sa propre version d'une partie de l'environnement (E) ainsi qu'un programme à exécuter (champ code). Nous considérerons aussi un processus particulier (une constante), noté π. L exécution d un algorithme représenté par un terme SaLSA sera définie comme une séquence de transitions dans le calcul de processus, en accord avec les règles qui suivent. Les transitions de ce système seront notées : ( ξ,{p 1,..., p n }) // ( ) = = = ξ',{p 1 ',..., p l '} ou un processus p j est soit distinct de l ensemble des p i, soit égal à l un des p i après substitution de certaines des entrées, ce qui sera noté : [ ] p' j = p i champ nouvelle valeur Nous sommes maintenant prêts à exposer les règles spécifiant quelles sont les transitions admissibles de ce système Transitions du calcul de processus La première règle fait le lien avec le langage de programmation hôte. L évaluation d une instruction SOLVE renvoie un Booléen indiquant si la recherche a atteint des état finaux. Ainsi, pour évaluer l'expression solve(t), on crée un processus p à partir de l'environnement courant, on le laisse évoluer suivant le terme T, jusqu'à ce qu'on aboutisse à un unique processus p', soit en état d'échec, soit en état fini. ( ) p': env = E' R state = startr p : R env = E R, (ξ, p) // R R R R (ξ, {p' }), R( p'.state = done v = true) R R ( p'.state = failed v = false) R R code = T R (ξ, E): solve(t) (ξ', E'): v Règle 1: lien avec le langage hôte La deuxième règle est la règle structurelle principale du calcul de processus. Elle indique l'indépendance des processus en autorisant à tout moment l'application d'une transition sur l'un quelconque des processus disponibles. Du point de vue opérationnel, ceci indique que les processus SaLSA associés à chaque nœud de l'arbre de recherche peuvent effectivement être implémentés par des processus d'un système distribué. Du point de vue de l'exploration d'un arbre de recherche, ceci correspond à la progression de la frontière des noeuds explorés dans un arbre. Cette règle montre que quand les processus ne sont pas en section critique (modification de l environnement global ξ), on peut les exécuter en parallèle. (ξ, p) (ξ, { p, p 1,..., p k }) // = = = (ξ, {p 1 ',..., p n '}) // = = = (ξ, { p 1,..., p k, p 1 ',..., p n '}) Règle 2: Des processus distribués La troisième règle indique comment expanser un noeud. On commence par générer l ensemble des valeurs possibles pour les variables libres. Les processus fils ne sont pas créés de manière instantanée, on 3
192 préfère commencer par associer à chacune de ces étiquettes un processus constant, inactif, π. Comme les processus fils sont crées en copiant l'environnement du processus-père, ce mécanisme permet de répercuter les conséquences d'un échec dans une branche au niveau du nœud père, et de là, aux autres nœuds-fils de celui-ci. C = Choice( f,g,h,k) R state = start R p : R env = E R, (ξ, E): f () (ξ, E): { x 1,...,x n }, n > 0 R code = C T 1 T 2 R // R state expand R (ξ, p) R R R (ξ, p Rsons {(x 1,π),...,(x n,π)} R ) Règle 3: génération des étiquettes d un noeud La règle 4 spécifie le comportement du choix quand la règle 3 n'a pas pu être appliquée puisque l'ensemble d'étiquettes généré est vide. On dit dans ce cas, que l'on est arrivé à un point fixe du choix. On cherche alors à appliquer les transitions associées à l'évaluation du sous-terme T 2. C = Choice( f, g,h, k) R state = start R p : R env = E, (ξ, E): f () (ξ, E): {} R R code = C T 1 T 2 R // (ξ, p) R R R (ξ, p[ code T 2 ]) Règle 4: arrivée à un point fixe pour le choix. La règle 5 correspond à la descente proprement dite dans une branche. On part d'un processus auquel on a déjà appliqué la règle 3 (son état vaut expand, et le champ son n'est pas vide). Cette règle est applicable tant qu'il existe une des branches qui contient le processus par défaut π, la règle 5 remplace ce processus par un nouveau p' dont l'environnement est obtenu à partir de celui de π, après évaluation de la fonction g associée à la descente dans la branche. (ξ, E): g(x i ) (ξ, E'): void (ξ, E): k() (ξ, E): true R state = expand R R father = p R env = E p : R R code = Choice( f,g,h,k) T 1 T p': R env = E' R R 2 R Rcode = T 1 R R sons = {(x 1, p 1 ),...,(x i,π ),..,(x n, p n )} R Rstate = startr // (ξ, p) R R R (ξ, p[ sons {(x 1, p 1 ),...,(x i, p' ),..,(x n, p n )}]) Règle 5: création du processus p en descendant dans la branche La règle 6 s'applique dans les mêmes conditions que la règle 5, mais considère le cas où soit la fonction g associée à la descente dans la branche a créé un échec (valeur ), soit quand la fonction de filtrage k, n'a 4
193 pas été vérifiée. Dans ces cas, on remplace le processus π correspondant à la branche x i par un nouveau processus p' en état d échec (failed). = p': state father = = failed p = = = = state = expand = env = E p : = code = Choice( f, g,h, k) T 1 T = = 2 = = sons = (x 1, p 1 ),...,(x i,π),..,(x n, p n ) { } = (ξ, p) (ξ, E): g(x i ) (ξ, E'): v (ξ, E) : k() (ξ, E) : v' v = v' = false ( ) (ξ, E): h(x i ) (ξ', E''): void // = env E'' = = = = (ξ', p = sons {(x 1, p 1 ),...,(x i, p' ),..,(x n, p n )} = ) Règle 6: échec dans une branche La règle 7 s'applique à un nœud qui a été expansé par la règle 3 puis dans toutes les branches duquel on a appliqué une règle 5 ou 6 (en cas d'échec). Dans ce cas, tous les fils d un noeud ont été expansés et le processus père se met en état d'attente (wait). = state = expand p : = = sons = {(x 1, p 1 ),...,(x n, p n )} =, i, x i π // (ξ, p) = = = (ξ, p[ state wait]) Règle 7: tous les fils ont été expansés Les règles 8 et 9 agissent de manière globale sur toute la frontière de l'arbre. Ces règles permettent d'exprimer la remontée des résultats des explorations dans chacune des branches d'un point de choix. La règle 8 inique que quand tous les fils d un noeud ont échoués, le noeud-père échoue lui aussi et tous ses fils disparaissent. i, p i.state = failed R state = wait p : R R sons = {(x 1, p 1 ),...,(x n, p n )} R // (ξ, { p, p 1,..., p n, p 1 ',..., p l '}) R R R (ξ',{ p[ state failed], p 1 ',..., p l '}) Règle 8: remontée des échecs La règle 9 indique que quand tous les fils d un noeud ont fini leurs calculs sans que tous aient échoué, leur père change d état (il a fini ses calculs) et les fils disparaissent. 5
194 i {2..n}, p i.state { failed, done} p 1.state = done = state = wait p : = = sons = {(x 1, p 1 ),...,(x n, p n )} = // = = = (ξ',{ p[ state done], p 1 ',..., p l '}) (ξ, { p, p 1,..., p n, p 1 ',..., p l '}) Règle 9: remontée des signaux de fin de calcul La règle 10 indique que quand un processus arrive à un terminateur exit, tous les calculs en cours dans les autres processus sont abandonnés. Il s'agit donc encore d'une règle globale agissant sur l'ensemble de la frontière. p 1.code = exit // (ξ, { p 1,..., p n }) = = = (ξ', { p 1 [ state done] }) Règle 10: terminateur exit La règle 11 indique que quand un processus arrive à un terminateur cont, ce processus disparaît. p 1.code = cont // (ξ, { p 1,..., p n }) = = = (ξ', { p 2,..., p n }) Règle 11: terminateur cont Ces onze règles ont permis de décrire le comportement d'expressions SaLSA simples. Les trois règles qui suivent traitent le cas d'expressions faisant intervenir des constructions plus complexes comme la limite en nombre de sorties ou la recherche en avant. La règle 12 explique le comportement de termes de la forme T / n. On crée une copie p' du processus courant p, dans laquelle on commence par suivre les règles d évolution normales pour le terme T. On obtient ainsi un certain nombres de processus dont certains sont dans un état d attente (état = start et code = ε: ils correspondent aux sorties de T dont on veut limiter le nombre à n). Si l on trouve n tels processus, on peut arrêter tous les autres et continuer à partir de ces n processus. Tant que l on n en a trouvé moins de n, on continue l évolution du calcul pour T. Si enfin, on arrive à un point où l on a un nombre m, 0 < m < n de processus en attente et où tous les autres processus sont soit des processus qui ont échoué (état = failed), soit des processus qui ont fini de s exécuter (état = done), soit des noeuds internes de l arbre (état = wait), alors, on ne garde que ces m sorties en attente. 6
195 // R state = startr (ξ, p') R R R (ξ', { p 1,..., p m, p 1 ',..., p l '}, m > 0 p': Renv = E R i {1..m}, ( p i.state = start, p i.code = ε) R code = T R R m = n R R state = start R R R m < n R p : Renv = E R R p Rcode = T / n T 1 T 2 R j {1..l}, R j '.state {failed,done, wait} R R R R R R R R Rp j '.code ε R R R // (ξ, p) R R R (ξ',{ p 1 [ code T 1 ],..., p m [ code T 1 ]}) Règle 12: exploration à nombre limité de sorties La règle 13 explique le comportement de termes de la forme smallest(t,f,n,r). On crée une copie p' du processus courant p, dans laquelle on commence par suivre jusqu à leur terme les règles d évolution normales pour le terme T. On distingue ensuite les m processus en état d attente (état = start et code = ε) des autres processus (état = failed, wait, done). Pour les m processus intéressants, on évalue la fonction f() dans leur environnement associé, et on ne retient que ceux d entre eux qui ont donné des valeur suffisamment proches de la valeur minimale. = state = start = p': = env = E = = code = T = // (ξ, p') = = = (ξ', { p 1,..., p m, p 1 ',..., p l '}, m > 0 i {1..m}, p i.state = start, p i.code = ε j {1..l}, p i.state wait, failed,done ( ) ( { }) = state = start = i {1..m}, (ξ, p i.env): f () (ξ, p i.env):a i p : = env = E = a 1 a 2... a m = code = smallest(t, f,n,r) T 1 T 2 = p = min( i {1..m} a i > a 1 / r) k = min(m,n, p) // (ξ, p) = = = (ξ', { p 1 [ code T 1 ],..., p m [ code T 1 ]}) Règle 13: Recherche en avant La règle 14 spécifie le comportement de termes de la forme smallest(t,f,n,r) ou bien T / n quand l exécution du terme T n a laissé aucun processus en état d attente. On considère dans ce cas que l on est a un point fixe pour le point de terme smallest(t,f,n,r) ou T / n 7
196 R state = startr // p': Renv = E R (ξ, p') R R R (ξ', { p'' }) R code = T R R C = T / n R R R C = smallest(t, f, n,r) R p'.state = failed R s = failed R R R Rc = ε R R R state = start R R p : Renv = E R p'.state = done R s = start R R R Rc = T 1 R R Rcode = C T 1 T 2 R // (ξ, p) R R R(ξ', R code c p'' R R R R Rstate s R R ) Règle 14: aucune sortie d une recherche en avant ou d une recherche limitée en nombre de sorties 12.3 Implémentation du générateur de code Un environnement d exécution mono-processus A partir de la sémantique opérationnelle présentée plus haut, un système a été implémenté au Laboratoire Central de Recherches de Thomson-CSF pour permettre d utiliser SaLSA lors de la description d algorithmes en CLAIRE. Pour garder une implémentation légère et facilement maintenable, nous n avons pas étendu le modèle de calcul CLAIRE. Ce modèle est très simple, il s agit d un calcul mono-processus, où une partie des données est stockée sur une pile : on a la possibilité, à tout instant, de revenir (pour les données qui sont stockées de la sorte) à un état antérieur du système. Ces états du système ne sont pas des objets, on ne peut pas les éditer, les passer en paramètre à des procédures ou les stocker autrement que sur cette pile. Il s agit donc d un modèle moins riche que les espaces de calcul d Oz [SS 94] où le fait de pouvoir nommer les états et les passer comme paramètres à des fonctions permet d obtenir un calcul distribué. Ce modèle de calcul ne permet ainsi que de faire de l exploration en profondeur d abord des arbres de recherche (et les stratégies d'exploration basées sur ce mode de visite, comme l exploration à profondeurs successives [Ko 85]). Ce choix d implémentation nous conduit à avoir des algorithmes économes en place mémoire, au prix d une certaine rigidité dans l exécution de ces algorithmes de recherche (par exemple, puisque l affectation des ressources de calcul aux processus distribués est un problème qui ne se pose pas, deux exécutions d un même algorithme donneront de fait toujours le même résultat). On doit considérer que le système réalisé jusqu à présent est extensible et pourrait être complété par d autres modes d exécution (ainsi, il serait par exemple naturel d avoir un générateur d algorithmes de recherche distribués dans un environnement JAVA). Le fait d exécuter du code dont la sémantique est exprimée dans un calcul de processus distribués sur un seul processus au lieu de plusieurs, nous a amené à simuler certains mécanismes naturels du calcul multiprocessus. Ainsi, pour le cas de la recherche en avant, où l on parcourt un arbre, pour ne continuer qu à partir des meilleures feuilles, les états associés aux feuilles de l arbre ne peuvent pas être gardés, car ils n existent pas comme objets manipulables par le langage. On en garde ainsi uniquement une représentation formelle (la suite des évaluations qui y ont conduit), et le cas échéant, on recalcule une deuxième fois ces états pour continuer l algorithme à partir d eux. 8
197 Choix d implémentation Quelles que soient les choix d implémentations, il fallait disposer à l exécution d une pile pour stocker des informations locales aux différents choix. Puisqu on peut associer à chaque choix une itération, il fallait bien stocker à chaque niveau de l arbre au moins une représentation de l ensemble en cours d itération. Nous avons considéré deux possibilités d implémentation: 1. la première consistait à obtenir l exécution de l algorithme comme l application d une méthode générique (un évaluateur spécialisé) aux termes syntaxiques SaLSA. Dans ce cas, l évaluateur aurait été responsable de la gestion des variables locales de chaque choix, et aurait implémenté une pile pour maintenir les états intermédiaires de toutes les itérations. 2. la seconde possibilité consistait à générer à l avance du code à partir des termes syntaxiques pour être ensuite totalement indépendant de ceux-ci au moment de l appel de l algorithme. De plus, on pouvait dans cette optique, générer des fonctions récursives et faire ainsi l économie de l implémentation de la pile qui correspondrait à la pile des appels de fonctions et des contextes d évaluation. La comparaison entre ces deux possibilités d implémentation ressemble à la question qui se pose pour les règles logiques, l approche RETE [Fo 82] étant dans la première optique alors que le compilateur de règles MARIE [Ca 87] se situe dans la seconde (on peut aussi dire que, pour la maintenance incrémentale d invariants, LOCALIZER [MvH 97] se situe dans la première approche). Chacune des deux approches a ses avantages et ses inconvénients. La première correspond à une approche traditionnelle de génie logiciel utilisant des objets et peut s implanter relativement aisément sous forme de librairie, alors que la seconde nécessite de faire de la génération de code (ce qui ne peut éventuellement être fait dans le cadre de librairies que si le langage est réflexif et offre un accès complet à ses structures internes). La première méthode peut être implémentée de manière efficace, cependant, elle ne peut atteindre l efficacité de la seconde pour deux raisons : 1. le code exécuté est un code générique qui découvre l algorithme au moment où il l exécute. Il doit donc comprendre des mécanismes de type pattern matching, alors que ce travail a été fait en prétraitement, au moment de la génération de code par la seconde approche. 2. D autre part, la première approche doit gérer elle même des structures. Elle sera donc amenée à reproduire d une manière ou d une autre la gestion d environnements lexicaux. La seconde approche peut utiliser la pile d appels fonctionnels du langage sous-jacent (ici, C), en codant les choix par des fonctions récursives. Ainsi, les variables locales (en particulier d itération) des choix sont des variables locales d une fonction, et le mécanisme d empilement des contextes est fournit gratuitement par le langage cible. On évite ainsi les indirections résultant du stockage des contextes dans des champs objets globaux, ce qui permet un code plus rapide et dans lequel on n'a pas à se préoccuper d'allocation ou de récupération de mémoire. Entre ces deux possibilités, nous avons choisi la seconde pour ne pas avoir de perte de performance par rapport aux algorithmes de recherche existants en CLAIRE. L implémentation se résume à un millier de lignes de code CLAIRE. Ceci a pu être possible grâce à l accessibilité du langage interne de CLAIRE. On peut en effet aisément créer des objets qui sont eux-mêmes des définitions de méthodes, d objets ou des instructions. Du point de vue de l utilisateur, cette génération de code à la volée permet à l'utilisateur de rentrer des expressions SaLSA à l interprète CLAIRE, et de voir leur code CLAIRE correspondant généré puis évalué. Quand CLAIRE est utilisé comme pré-processeur de C++, les expressions SalSA sont remplacées automatiquement par leur expansion en CLAIRE avant d être compilées en C++. Ce choix a eu d autres avantages que ceux de l efficacité. Étant dans la lignée d autres possibilités de génération de code offertes en CLAIRE (gestion des expressions ensemblistes, mécanisme ouvert 9
198 d itérations, expansion inline de méthodes, etc. ), le code produit par le générateur de code peut lui même, à son tour, profiter de mécanismes d optimisation du code Claire. On en donne un exemple ci-dessous. Ce code correspond à la procédure de Lin et Kernighan, présentée au paragraphe Cette procédure avait été décrite par le terme suivant SOLVE(largest(Init (Augment (Augment / 1)*), gain, 1, 1.0)* Les points de choix ont été définis de la manière suivante. exit) Init :: Choice( moves init_cycle(x) with x in unhappy_cities()) Augment :: Choice( moves add_to_cycle() with x in all_cities() ) Le compilateur SaLSA génère deux définitions de fonctions, LO_1 et LO_2. L'exécution du terme cidessus correspond à un appel à LO_1. On remarquera que le code généré fait appel à une structure de contrôle spécifique à Claire: branch. Cette construction permet de poser un point de choix puis poursuivre l'exécution du code dans une branche hypothétique. En cas d'échec (levée d'une exception de type contradiction) dans cette branche hypothétique, on restaure l'état du système tel qu'il était au moment de la pose du point de choix. On remarquera aussi que le code généré fait souvent appel à des fonctions dont le nom est préfixé par SaLSA (SaLSA/pushPATH, etc.). Ces fonctions correspondent à une bibliothèque d'utilitaires pour la sauvegarde d'un chemin dans l'arbre de recherche, et le retour à un état précedemment visité, à partir de la donnée du chemin qui permet d'accéder à cet état. 10
199 [LO_1() -> SaLSA/InitLookAhead(1, 100, true), let n1 := 0 in (if (for y1 in unhappy_cities() (if branch((init_cycle(y1), SaLSA/pushPATH(y1), n1 :+ 1, let n:integer := 0 in (if (for y in all_cities() (if branch((add_to_cycle(y), SaLSA/pushPATH(y), n :+ 1, LO_2())) break(true))) true else if (n = 0) (SaLSA/pushPATHunknown(), SaLSA/register_max(gain), false)))) break(true))) true else if (n1 = 0) (SaLSA/pushPATHunknown(), SaLSA/register_max(gain), false)), if (SaLSA/Gval(SaLSA/LblSize) <= 0) true else let l := nil in (for i in (1.. SaLSA/Gval(SaLSA/LblSize)) (if SaLSA/big_enough(i) l :add SaLSA/Glval(SaLSA/LblList)[i] else break(false)), for %path in l (if branch((init_cycle(%path[1]), add_to_cycle(%path[2]), let n := 3 in while (%path[n]!= unknown) (add_to_cycle(%path[n]), n :+ 1), domove(), LO_1())) break(true))) ] [LO_2() -> let n1 := 0 in (if (for y in all_cities() (if branch((add_to_cycle(y), SaLSA/pushPATH(y), n1 :+ 1, if (n1 > 1) contradiction(), LO_2())) break(true))) true else if (n1 = 0) (show_path(), SaLSA/pushPATHunknown(), SaLSA/register_max(gain), false))] Interface d utilisation L interface d utilisation actuelle présente quelques différences de syntaxe avec le langage tel qu il a été présenté ici. Les premières différences sont dues à la limitation du jeu de caractères ascii (les opérateurs, =et sont ainsi remplacés par *, /+ et in; de même, les termes C n et C* sont dénotés C^n et C^*). La seconde différence est plus importante que cette simple différence typographique. Elle revient à présenter la création de choix plus explicitement comme des combinateurs d ordre supérieur. En effet, pour rentrer dans le système un choix, on doit remplacer dans sa description toutes les expressions comportant des variables libres par des appels fonctionnels (prenant en paramètres l ensemble des variables libres que l expression serait susceptible d utiliser). Cette différence est due uniquement à des 11
200 raisons historiques (nous avons été inspirés par la présentation naturelle de LOCALIZER après avoir réalisé l implémentation) et sera vraisemblablement levée dans le futur. Comme nous l avons vu au cours de cette partie, un avantage de cette description concise des algorithmes hybrides est la possibilité qui est offerte à l utilisateur de modifier facilement le contrôle d un algorithme, pour rajouter un peu d exploration globale (LDS) ou une recherche taboue, ou simplement une trace d information, etc. Ces transformation sont non seulement faciles (puisqu il s agit de rajouter quelques lignes à quelques choix ou de modifier un terme), elles sont aussi encapsulables sous forme de fonctions puisque les choix et les expressions SaLSA sont réifiées. Nous prévoyons de développer une série de fonctions utilitaires de contrôle, prenant en paramètre (et renvoyant) une expression SALSA pour mettre facilement à disposition ces technologies algorithmiques. Les applications de telles fonctions sont multiples (trace, visualisation, méta-heuristiques de contrôle, explication, statistiques,...) 12
201 13. Conclusion La première partie de ce travail a permis de dresser une cartographie de nombreux problèmes combinatoires, du point de vue de leur résolution par des techniques de programmation par contraintes ou par des algorithmes de Recherche Opérationnelle. Cette cartographie a été reprise de manière synthétique dans le chapitre 8. On a ainsi montré que la programmation par contraintes pouvait être une technique compétitive pour la résolution de nombreux problèmes, en particulier, à l'intérieur d'approches hybrides combinant la P.P.C. avec d'autres techniques (méthodes constructives, recherche globale, optimisation locale, recherche LDS). Ainsi, nos expérimentation ont débordé du cadre initial de la programmation par contraintes pour envisager des algorithmes hybrides. Ces expériences ont été possibles puisque l'on ne s'était pas limité, dès le départ, à l'utilisation d'un système particulier. Cette méthode a eu l'avantage de permettre l'obtention de très bons résultats que nous n'aurions pas pu obtenir si nous avions été limité à l'utilisation d'un système de contraintes classique. En contrepartie, le second objectif de la thèse -mettre ces techniques à la disposition d'utilisateurs dans un système pour la résolution de problèmes d'optimisation combinatoire- s'est trouvé plus difficile à atteindre suite à l'élargissement du champ des techniques algorithmiques pertinentes. En effet, l'environnement à proposer s'est trouvé soumis à deux exigences contradictoires : d'une part, il fallait offrir des techniques assez différentes et ne cohabitant pas facilement ensemble; d'autre part, il fallait proposer un cadre suffisamment ouvert pour que l'utilisateur puisse continuer à expérimenter de nouvelles techniques, comme nous l'avions fait. En effet, les expérimentations que nous avons faites montrent combien le développement d'algorithmes hybrides est un champ prometteur et nos modestes expériences ne peuvent en aucun cas être considérées comme définitives. La première possibilité, que nous avions envisagée au début de ce travail, consistait à proposer un langage de modélisation, (par exemple une algèbre de contraintes spécialisée pour la résolution de problèmes d'optimisation combinatoire) avec des opérateurs et des objets prédéfinis (comme des tâches, des ressources, des grilles d'emplois du temps, des matrices de distance, etc.) et à animer un tel modèle par les algorithmes qui nous seraient apparus opportuns. Un tel système aurait sans doute pu être implémenté si nous étions peu sortis du cadre de la P.P.C., par exemple si tous les bons algorithmes avaient été des recherches en branch and bound. Comme cela n'a pas été le cas, un tel système devenait beaucoup plus complexe à réaliser. Le travail réalisé constitue un premier pas dans cette direction. Tout d'abord, il nous a semblé qu'il fallait laisser à l'utilisateur la possibilité de continuer à faire le même type d'expérimentations que celles que nous avions faites, en lui laissant le contrôle complet sur l'algorithme de résolution. Un tel cadre, plus ouvert qu'une boite à outils combinatoires, semblait moins courir le risque (inhérent à toute collection de méthodes) de devenir rapidement obsolète. Pour trouver quel serait un langage ou un système idéal pour la résolution de problèmes combinatoires, nous sommes partis de CLAIRE. En effet, notre productivité en quatre ans montre que CLAIRE est un bon système pour développer rapidement des algorithmes très efficaces et qu'il n'y manque rien d'essentiel. Ce qui manque à Claire pour rendre encore plus facile la programmation d'algorithmes complexes, c'est un peu d'abstraction et de déclarativité pour la description d'algorithmes. Nous avons exploré deux pistes pour y remédier. D'une part nous avons créé ECLAIR, une bibliothèque pour la manipulation aisée de contraintes sur les domaines finis. D'autre part, nous avons proposé SaLSA, un langage et son compilateur pour la description élégante et synthétique d'algorithmes hybrides combinant recherche locale et recherche globale. L'utilisation d'eclair est aisée, car il s'agit d'une bibliothèque (l'utilisateur n'a donc pas besoin d'apprendre un nouveau langage) et l'écriture de contraintes suit une syntaxe naturelle et intuitive. Le 1
202 grand avantage d'une telle bibliothèque est d'offrir la possibilité de modéliser un problème par un programme très court. On retrouve ainsi un pouvoir de modélisation très fort, puisque l'on peut profiter à la fois de la modélisation orientée-objets, de l'utilisation aisée des ensembles et des relations quelconques et de la pose de contraintes de manière déclarative. On combine ainsi l'agrément des systèmes de modélisation par entités-relations et du cadre déclaratif de la programmation logique par contraintes. De plus, comme d'autres systèmes de P.P.C. offerts sous forme de bibliothèque, ECLAIR est aisément extensible par de nouvelles contraintes ou de nouveaux modes de représentation des variables. A chaque fois que l'on dispose d'un algorithme de propagation incrémental, on peut le mettre aisément à disposition sous forme d'une contrainte, ce qui en facilite la réutilisation ultérieure. Si la bibliothèque ECLAIR n'est pas une technologie très originale, le langage SaLSA l'est beaucoup plus. Spécifier la stratégie de contrôle de la recherche de manière indépendante de la déclaration du problème est une vieille idée de programmation logique qui n'a malheureusement quasiment jamais été offerte dans les systèmes implantés. Dans le cadre de la stricte programmation par contraintes, ECLAIR permet de modéliser de manière élégante et déclarative le problème à résoudre, et SaLSA permet de spécifier la manière dont on construit et on visite l'arbre de recherche. SaLSA permet aussi de sortir du cadre de la P.P.C. stricto sensu pour programmer des algorithmes hybrides. On peut ainsi programmer des algorithmes mêlant recherche locale et globale en utilisant CLAIRE pour la modélisation du problème, ECLAIR pour la pose des contraintes et SaLSA pour la définition des voisinages et pour le contrôle global de l'algorithme. On peut aussi considérer d'autres algorithmes hybrides, mêlant des composants impératifs avec d'autres composants effectuant des recherches (et donc des choix et des retours en arrière). Quels que soient ces autres composants algorithmiques programmés en CLAIRE, on pourra utiliser SALSA comme langage permettant de connecter ces composants les uns aux autres en spécifiant le flot de contrôle global. De plus, notre contribution avec la définition du langage SaLSA est complètement indépendante du langage cible du compilateur, ici CLAIRE. La définition de ce langage de spécification du flot de contrôle d'algorithmes de recherche hybrides peut être utilisée dans n'importe quel environnement au dessus d'autres langages de programmation. Le travail lié à SaLSA s'inscrit ainsi dans le sillon des travaux associés à la génération de code, et dont le but est de faciliter la réutilisation d'algorithmes. Pour reprendre la question posée dans le sujet de la thèse, nous proposons d'aider de deux manières un utilisateur ayant à résoudre un problème combinatoire complexe. D'une part, nous avons dressé une petite cartographie de diverses techniques de résolution et en particulier de la P.P.C. sur un ensemble de problèmes. Cette cartographie devrait permettre à cet utilisateur de choisir le type d'algorithme à mettre en œuvre sur son problème. D'autre part, nous proposons une plate-forme pour le développement d'algorithmes hybrides autour de CLAIRE. Cette plate-forme contient aujourd'hui : = le langage CLAIRE, un langage de programmation simple, pour écrire du code élégant, = une librairie d'algorithmes en optimisation combinatoire correspondant aux algorithmes décrits dans la partie A, = la langage de règles de production MARIE [Ca 87], pour la programmation d'inférences complexes, = la librairie ECLAIR, pour la programmation de contraintes sur les domaines finis, = et le langage SALSA, pour la programmation du flot de contrôle dans des algorithmes hybrides complexes. Il s'agit donc d'un environnement hybride où l'on peut programmer des algorithmes à différents niveaux d'abstractions. L'utilisateur n'est donc prisonnier d'aucun système et a toujours la possibilité de programmer des algorithmes impératifs en CLAIRE. Mais, s'il veut utiliser un algorithme qui peut être spécifié de manière déclarative à l'aide de règles, contraintes, ou des bibliothèques de classes, alors il aura 2
203 la liberté d'utiliser les librairies et langages offerts au dessus de CLAIRE, et de profiter du travail de compilation soignée qui a déjà été fait pour ces composants. Si l'algorithme à programmer ne rentre pas dans ce cadre, il disposera de CLAIRE. 3
204 5
205 14. Bibliographie [ACC 93] M. Abadi, L. Cardelli, P.-L. Curien, Formal Parametric Polymorphism, Theoretical Computer Science 121 (1-2) p. 9-58, [ABZ88] J.Adams, E. Balas, D. Zawak, The Shifting Bottleneck Procedure for Job Shop Scheduling, Management Science 34, p , [AB91] A. Aggoun, N. Beldiceanu, Overview of the CHIP Compiler, Logic Programming, Proc. of the 8 th International Conference on Logic Programming, Paris, K. Furukawa ed., p , MIT Press, Cambridge, [AB 93] A. Aggoun, N. Beldiceanu, Extending CHIP in order to Solve Complex Scheduling and Placement Problems, Mathematical and Computer Modelling 17 (7), p , [AKP 90] H. Aït-Kaci, A. Podelski, The Meaning of Life, PRL Research Report, Digital, [AVT 89] R. Alvarez-Valdes, J.M. Tamarit, Heuristic Algorithms for Resource-Constrained Project Scheduling: a Review and an Empirical Analysi,s in R. Slowinski, J. Weglarz (eds): Advances in Project Scheduling, Elsevier, Amsterdam, p , [ABCC 94] D. Applegate, R. Bixby, V. Chvatal, W. Cook, Finding Cuts for the TSP, published electronically at ftp://netlib.att.com/netlib/att/math/applegate/tsp/tsp_aug23.ps.z [AC 91] D. Applegate, W. Cook, A Computational Study of the Job-Shop Scheduling Problem, Operations Research Society of America 3 (2), p , [Ap 97] K.R. Apt, From Chaotic Iteration to Constraint Propagation, Proc. of the 24 th International Colloquium on Automata, Languages and Programming (ICALP'97), Lecture Notes in Computer Science 1256, p , Springer, [Au 95] P. Augerat, J.M. Belenguer, E. Benavent, A. Corberan, D. Naddef, G. Rinaldi, Computational Results with a Branch and Cut Code for the Capacitated Vehicle Routing Problem, Artemis Imag Research Report RR949-M, institut Imag, Grenoble, [Ba 69] E. Balas, Machine Sequencing via Disjunctive Programming: an Implicit Enumeration Algorithm, Operations Research 17, p , [BT 86] E. Balas, P. Toth, Branch and Bound Methods, in [Law86], p [BLN 95] P. Baptiste, C. Le Pape, W. Nuijten, Constraint-based Optimization and Approximation for Job Shop Scheduling, Proc. of the AAAI-SIGMAN Workshop on Intelligent Manufacturing Systems, IJCAI'95, Montréal, Quebec, [BL 95] P. Baptiste, C. Le Pape, A Theoretical and Experimental Comparison of Constraint Propagation Techniques for Disjunctive Scheduling. Proc. of the 14 th International Joint Conference on Artificial Intelligence, Montréal, Quebec, [BL 96] P. Baptiste, C. Le Pape, Edge-Finding Constraint Propagation Algorithms, Proc. of the ECAI workshop on Intelligent Scheduling of Production Processes, [BL 96b] P. Baptiste, C. Le Pape, Constraint Propagation Techniques for Disjunctive Scheduling : the Preemptive Case, proc. 12 th European Conference on Artificial Intelligence, [BdL 96] E. Barducci, A. del Lungo, M. Nivat, R. Pinzani, Reconstructing Convex Polyominoes from Horizontal and Vertical Projections, Theoretical Computer Science 155,
206 [BaC 94] J.W. Barnes, J.B. Chambers, Solving the Job-Shop Scheduling Problem using Tabu Search, IIE Transactions 27, [Bea 87] J.E. Beasley, An Algorithm for Set Covering Problem, European Journal of Operations Research 31, p , [Bea 93] J.E. Beasley, Lagrangean Relaxation, p , in [Ree 93]. [BB 96] N. Beldiceanu, E. Bourreau, D. Rivreau, H. Simonis, Solving Resource-Constrained Project Scheduling Problems with CHIP, 5 th International Workshop on Project Management and Scheduling, Poznan, Poland, [BC 94] N. Beldiceanu, E. Contejean, Introducing Global Constraints in CHIP, Mathematical Computer Modelling 20 (12), p , [Bo 83] L. Bodin, B. Golden, A. Assad, M. Ball, Routing and Scheduling of Vehicles and Crews, the state of the art, Special issue of Computers and Operations Research 10 (2), p , Pergamon Press, [BG 61] R.G. Busaker, P.J. Gowen, A Procedure for Determining a Family of Minimal Cost Network Flow Patterns, O.R.O. Technical Report 15, Johns Hopkins University, [CL 91] J. Carlier, B. Latapie, Une Méthode Arborescente pour résoudre les problèmes cumulatifs, Operations Research 25 (3), p , [CC 88] J. Carlier, P. Chrétienne, Problèmes d'ordonnancement, ERI Masson, Paris, [CP 89] J. Carlier, E. Pinson, An Algorithm for Solving the Job Shop Problem, Management Science, 35 (2), p , [CP 94] J. Carlier, E. Pinson, Adjustments of Heads and Tails for the Job-Shop Problem, European Journal of Operations Research 78, p , [CP 96] J. Carlier, E. Pinson, Jackson s Pseudo Preemptive Schedule for the Pm/r i,q i /C max Scheduling Problem, Rapport technique, HEUDIASIC, Compiègne, France, [Ca 86] M. Carter, A Survey of Practical Applications of Examination Timetabling Algorithms, Operations Research 34, [Ca 87] Y. Caseau, Etude et réalisation d'un langage objet: LORE, Thèse, Université de Paris Sud, [CGL 95] Y. Caseau, P.-Y. Guillo, E. Levenez, A Deductive and Object-Oriented Approach to a Complex Scheduling System, Deductive and Object-Oriented Databases, Proc. of the 3 rd International Conference, (DOOD'93), Phoenix, 1993, S. Ceri, K. Tanaka, S. Tsur eds., Lecture Notes in Computer Science 760, p , Springer, 1993, et Journal of Intelligent Information Systems, 4, 1995 [CK 97] Y. Caseau, T. Kökeny, An Inventory Management Problem, Note Technique de la Direction des Technologies Nouvelles, Bouygues, 11 av. E. Freyssinet, St. Quentin en Yvelines, France, [CK 92] Y. Caseau, P. Koppstein, A Rule-Based Approach to a Time-Constrained Traveling Salesman Problem. 2 nd Int. Symp. on A.I. and Mathematics, 1992, Bellcore Technical Memorandum, [Ca 91a] Y. Caseau, An Object-Oriented Language for Advanced Applications. Proc. of TOOLS USA 91, Prentice Hall, [Ca 91b] Y. Caseau, A Deductive Object-Oriented Language. Annals of Mathematics and Artificial Intelligence, Special Issue on Deductive Databases, march [Ca 94] Y. Caseau, Constraint Satisfaction with an Object-Oriented Knowledge Representation Language. Applied Intelligence, vol. 4, (2), may
207 [Ca 97] Y. Caseau, C++ Peut-il Concilier Haut Niveau d'abstraction et Efficacité? Note Technique de la Direction Scientifique, Bouygues, [CL 93] Y. Caseau, F. Laburthe, Object Oriented Formal Specification with Cecile, rapport du magistère MMFAI, École Normale Supérieure, 45 rue d'ulm, Paris, France, [CL 94] Y. Caseau, F. Laburthe, Improved CLP Scheduling with Task Intervals, Proc. of the 11 th International Conference on Logic Programming, P. Van Hentenryck ed., The MIT Press, p , [CL95a] Y. Caseau, F. Laburthe, Disjunctive Scheduling with Task Intervals, LIENS Technical Report 95-25, École Normale Supérieure, 45 rue d'ulm, Paris, France, [CL95b] Y. Caseau, F. Laburthe, Improving Branch and Bound for Job-Shop Scheduling with Constraint Propagation, Proc. Combinatorics and Computer Science (CCS 95), M. Deza, R. Euler, Y. Manoussakis eds., Lecture Notes in Computer Science 1120, Springer, [CL 96a] Y. Caseau, F. Laburthe. The CLAIRE Programming Language, LIENS Technical Report 96-16, École Normale Supérieure, 45 rue d'ulm, Paris, [CL 96b] Y. Caseau, F. Laburthe, Cumulative Scheduling with Task Intervals, Proc. of the Joint International Conference and Symposium on Logic Programming, M. Maher ed., The MIT Press, p , [CL 97a] Y. Caseau, F. Laburthe, Solving Small TSPs with Constraints, Proc. of the 14 th International Conference on Logic Programming, L. Naish ed., The MIT Press, [CL 97b] Y. Caseau, F. Laburthe, Solving Various Matching Problems with Constraints, Proc. of the 3 rd International Conference on Principles and Practice of Constraint Programming, CP'97, G. Smolka ed., Lecture Notes in Computer Science 1330, Springer, p , [CL 97c] Y. Caseau, F. Laburthe, A Constraint-based approach to the RCPSP, Proc. of Workshop on Industrial Directed Scheduling of CP'97, A. Davenport ed., [CL 97d] Y. Caseau, F. Laburthe, Heuristics for Large Constrained Vehicle Routing Problems, soumis pour publication au Journal of Heuristics, juin [CL 97e] Y. Caseau, F. Laburthe, SaLSA: A Specification Language for Search Algorithms, Rapport Technique du LIENS 97-11, École Normale Supérieure, 45 rue d'ulm, Paris, [Ch 85] N. Christophides, Vehicle Routing, in [Law 86], p [CAT 87] N. Christofides, R. Almarez-Valdes, J.M. Tamarit, Project Scheduling with Resource Constraints: a Branch and Bound Approach, European Journal of Operations Research, 29 (3), p , [Chv 73] V. Chvatal, Edmonds Polytopes and a Hierarchy of Combinatorial Problems, Discrete Mathematics 4, p , [Chv 83] V. Chvatal, Linear Programming, Freeman, New York, [CW 64] G. Clarke, J.W. Wright, Scheduling of Vehicles from a Central Depot to a Number of Delivery Points, Operations Research 12, p , [CW 88] T. Cooper, N. Wogrin, Rule-based Programming with OPS5. Morgan Kaufmann publishers, [CLR 86] T. Cormen, C. Leiserson, R. Rivest, Introduction to Algorithms. The MIT Press, [CC 77] P. Cousot, R. Cousot, Abstract Interpretation: a Unified Lattice Model for Static Analysis of Programs by Construction or Approximation of Fixpoints. Conference record of the 4 th annual 171
208 ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, p , ACM Press, New York, [Co 94] P. Cousot, Sémantique Notes de cours, DEA Informatique Mathématique et Applications, École Normale Supérieure, 45 rue d'ulm, [DF 54] G.B. Dantzig, D.R. Fulkerson, S.M. Johnson, Solution of a Large-Scale Traveling Salesman Problem, Operations Research, 2, p , [dbb93] B. de Backer, H. Beringer, A CLP Language Handling Disjunctions of Linear Constraints. Logic Programming, Proc. of 10 th International Conference on Logic Programming, D. Warren ed., p , The MIT Press, [DR 96] R. Davies, G. Royle, Graph Domination, Tabu Search and the Football Pool Problem, Technical Report, dept. of Cumputer Science, Universiy of Western Australia, [DT 93] M. Dell Amico & M. Trubian. Applying Tabu-Search to the Job-Shop Scheduling Problem. Annals of Operations Research, 41, p , [De 92] E. Demeulemeester, Optimal Algorithms for various classes of multiple resource constrained project scheduling problems, unpublished PhD. Dissertation, 180 p., Université Catholique de Louvain, Belgique, [DH 95] E. Demeulemeester, W. Herroelen, New Benchmark Results for the Resource Constrained Project Scheduling Problem, Research Report 9521, Department of Applied Economics, K.U. Leuven, Belgium, [DDI 94] G. Desaulniers, J. Desrosiers, I. Ioachim, M. Solomon, F. Soumis, A Unified Framework for Deterministic Time Constrained Vehicle Routing and Crew Scheduling Problems, cahiers du Gerad, G-94-46, École des Hautes Études Commerciales, Montréal, H3T 1V6, [DDS92] M. Desrochers, J. Desrosiers, M. Solomon, A New Optimisation Algorithm for the Vehicle Routing Problem with Time Windows, Operations Research, 40 (2), p , [dw 85] D. de Werra, An Introduction to Timetabling, European Journal of Operational Research, 19, p , [Di 59] E.W. Dijkstra, A Note on Two Problems in Connection with Graphs, Numerische Mathematik 1, p , [DP 94] U. Dorndorf, E. Pesch, Evolution-Based Learning in a Job-Shop Scheduling Environment, Computers and Operations Research 22, p , [Du 95] Dumas et al. An Optimal Algorithm for the Traveling Salesman Problem with Time Windows, Operations Research 43, p , [DW90] M. Dyer & L.A. Wolsey, Formulating the Single Machine Sequencing Problem with Release Dates as a Mixed Integer Program. Discrete Applied Mathematics 26, p , [Edm 71] J. Edmonds, Matroids and the greedy algorithm Mathematical Programming 1, p , [ELT 91] J. Erschler, P. Lopez, C. Thuriot, Raisonnement temporel sous contraintes de ressource et problème d ordonnancement, Revue d Intelligence Artificielle, 5 (3), [Fa 95] F. Fages, Constructive Negation by Pruning, LIENS technical report 95-24, École Normale Supérieure, 1995 et Journal of Logic Programming, 32 (2), p , 1997 [Fa 96] F. Fages, Programmation Logique par Contraintes, Ellipses, Paris,
209 [FF 56] L.R. Ford, D.R. Fulkerson, Maximal Flow through a Network, Candidan Journal of Mathematics, p. 399, [Fo 82] C.L. Forgy, Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem Artificial Intelligence, 19, p , [FG 93] R. Fourer, D. MacGay, B.W. Kernighan, AMPL: A Modelling Language for Mathematical Programming, Duxbury Press, Brook/Cole Publishing Company, [Ka 84] N. Karmarkar, A New Polynomial Algorithm for Linear Programming, Combinatorica, 4(4), p , p [Kh 79] L.G. Khachian, A Polynomial Algorithm in Linear Programming, Dokadly Academiia Nauk, SSR 244, p , [GJ 79] M. Garey, D. Johnson, Computers and Intractability: A Guide to the Theory of NP-Completeness, Freeman [GHL92] M. Gendreau, A. Hertz, G. Laporte, New Insertion and Postoptimization Procedures for the Traveling Salemsan Problem, Operations Research 40 (6), p , [GHL94] M. Gendreau, A. Hertz, G. Laporte, A Tabu Search Heuristic for the Vehicle Routing Problem, Management Science, 40, p , [Ge 94] B. Gerards, Macthing, in Handbook of Operations Research and Management Science (networks), M.O. Ball, T.L. Magnanti, C.L. Monma et G.L. Nemhauser eds., [Glo 89] F. Glover, Tabu Search, Part I, O.R.S.A. Journal on Computing 1, p , 1989, Tabu Search, Part II,, O.R.S.A. Journal on Computing 2, p. 4-32, [Go 58] R.E. Gomory, Outline of an Algorithm for Integer Solutions to Linear Programs, Bulletin of the American Mathematical Society 64, p , [GM 95] M. Gondran, M. Minoux, Graphes et Algorithmes, Eyrolles, Paris, 1995 (première édition en 1979) et traduction Graphs and Algorithms Wiley, New York, [Got 93] GOTHA (J. Carlier et al.), Les Problèmes d'ordonnancement, Recherche Opérationnelle, Operations Research, 27 (1), p , [Grö 80] M. Grötschel, On the symmetric Traveling Salesman Problem: solution of a 120-city Problem Mathematical Programming Studies 12, p , [GN 97] P. Gritzmann, M. Nivat, Discrete Tomography : Algorithmes and Complexity, Dastuhl Seminar Report 165, IBFI, Schloss Dagstuhl, Wadern, D-66687, Allemagne, janvier [HG 95] W. Harvey, M. Ginsberg, Limited Discrepancy Search, Proceedings of the 14th IJCAI, p , Morgan Kaufmann, [HK 71] M. Held, R. Karp, The Traveling Salesman and Minimum Spanning Trees Operations Research 18, p , 1970 et Math. Programming 1, p. 6-25, [HeKoe 96] J. Hendler, J. Koehler (editeurs), Control of Search in AI Planning, Dagstuhl seminar report 161, IBFI, Schloss Dagstuhl, Wadern, D-66687, Allemagne, novembre [Ho 75] J.H. Holland, Adaptation in Natural and Artificial Systems, Ann Arbor, The University of Michigan Press, [JLJ 97] E. Jacopin, F. Laburthe, J. Jourdan, Algorithmes Incrémentaux, rapport final du contrat DRET , Laboratoire Central de Recherches, Thomson-CSF, domaine de Corbeville, 91404, Orsay, France, mars
210 [JL 87] J. Jaffar, J.-L. Lassez, Constraint Logic Programming, Conference record of the 14 th annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, p , ACM Press, New York, [JM 94] J. Jaffar, M. Maher, Constraint Logic Programming : a Survey, Journal of logic Programming 19-20, p , [Joh 90] D.S. Johnson, Local Optimization and the Travelling Salesman, Proc. of the 17 th International Colloqium on Automata, Languages and Programming, E. Paterson ed., Lecture Notes in Computer Science 442, p , Springer, [Jou 95] J. Jourdan, Concurrence et Coopération de Modèles Multiples dans les Langages CLP et CC: vers une Methodologie de Programmation par Modélisation, thèse de l'université Paris VII, 2 place Jussieu, 75005, Paris, France, [Ka 72] R.M. Karp, Reducibility among Combinatorial Problems, in Complexity of Computer Applications, R.E. Miller, J.W. Thatcher eds. p , Plenum, N.Y., [KBZ 97] T. Kasper, A. Bockmayr, T. Zajac, Reconstructing Polyominoes with Constraint Programming, in [GN 97]. [KB 96] J. Kim, K. Benner, Implementation Patterns for the Observer Pattern, Pattern Langages of Program Design 2, J. Vlissides, J. Coplien, N. Kerth eds. Addison-Wesley, [KS 97] G. Kindervater, M. Saveslbergh, Vehicle Routing: Handling Edges Exchanges, in Local Search in Combinatorial Optimization, E. Aarts, J.K. Lenstra eds., Wiley, [KGV 83] S. Kirkpatrick, C.D. Gellat, M.P. Vecchi, Optimization by Simulated Annealing, Science 220, p , [KS 93] N. Klarlund, M. Schwartzbach, Graph Types, Conference record of the 20 th annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, p , ACM Press, New York, [KS 95] R. Kolish, A. Sprecher, A. Drexl, Characterization and Generation of a General Class of Resource Constrained Project Scheduling Problems, Management Science 41, [KB 95] G. Kontoravdis, J. Bard, A GRASP for the Vehicle Routing Problem with Time Windows, ORSA Journal on Computing, 7 (1), p , [Ko 85] R. Korf, Depth-first Iterative Deepening : an Optimal Tree Search, Artificial Intelligence 27 (1), p , [Kr 56] J. B. Kruskal, On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem, Proc. of the American Mathematical Society 7, p , [Ku 55] H.W. Kuhn, The Hungarian method for the assignment problem, Naval Research Quarterly 2, p. 83, [Lab 96] F. Laburthe, Ecrire du Code Élégant Pour Décrire des Algorithmes Complexes, actes du colloque Langages et Modèles à Objet, LMO'96, Y. Dennebouy ed., École Polytechnique Fédérale de Lausanne, [Lab 97] F. Laburthe, Maintaining Prolog-based Large Scale Combinatorial Optimization Software: a contribution to CHIC-2 methodology, note technique du LIENS, École Normale Supérieure,
211 [LDJS 98] F. Laburthe, P. Savéant, S. De Givry, J. Jourdan, Eclair: A Library of Constraints over Finite Domains, Technical Report ATS 98-2, Laboratoire Central de Recherches, Thomson-CSF, domaine de Corbeville, Orsay, France, [Lap 92] G. Laporte, The Vehicle Routing Problem: an overview of exact and approximate algorithms, European Journal of Operationnal Research, 59, p , [Lap 97] G. Laporte, Recent Advances in Routing Algorithms, rapport technique G-97-38, Les cahiers du GERAD, Centre de Recherche sur les Transports, École des Hautes Études Commerciales, Montréal, H3T 1V6, [Lau 78] J.-L. Laurrière, Alice: A Language and a Program for Stating and Solving Combinatorial Problems, Artificial Intelligence 10, p , [Law 86] E.L. Lawler et al. The Travelling Salesman, a Guided Tour or Combinatorial Optimization, Wiley Interscience, [Law 84] S. Lawrence, Resource Constrained Project Scheduling: an Experimental Investigation of Heuristic Scheduling Techniques, GSIA, Carnegie Mellon University [LCR 95] B. Le Cun, C. Roucairol and the PNN team, Bob: a Unified Platform for Implementing Branch and Bound Like Algorithms, research report RR 95-16, Laboratoire PRiSM, Université de Versailles, 45 av. des Etats Unis, Versailles, France, [LP 94] C. Le Pape, Implementation of Resource Constraints in ILOG SCHEDULE: A Library for the Development of Constraint-Based Scheduling Systems, Intelligent Systems Engineering, 3 (2), p , [LK 73] S. Lin, B.W. Kernighan, An Effective Heuristic for the Traveling Salesman Problem. Operations Research 21, p , [Lo 96] H. Lock, Optimizing the Cumulative Constraint by Global Capacity Constraints, Proc. Planen und Konfiguren, J. Sauer ed., Infix Verlag, [Lo 91] Lopez, Approche énergétique pour l ordonnancement de tâches sous contraintes de temps et de ressources, thèse de l'université Paul Sabatier, Toulouse, [mat 97] K. Mac Aloon, C. Tretkoff, Optimization and Computational Logic, J. Wiley, New York, [MM92] T. McClain, Mazzola, Operations Management, Prentice Hall, [Ma 77] A.K. Mackworth, Consistency in networks of relations, Artificial Intelligence, 8, p , [Mau 92] J.-F. Maurras, Programmation Linéaire, notes de cours de DEA, Faculté de Luminy, Université de Provence, [MS 96] D. Martin, P. Shmoys, A time-based approach to the Jobshop problem, Proc. of IPCO'5, Vancouver 1996, M. Queyranne ed., Lecure Notes in Computer Science 1084, p , Springer, [Mey 92] B. Meyer, Eiffel: the Language, Prentice Hall, [MM 94] A. Mingozzi, V. Maniezzo, S. Ricciardelli, L. Bianco, An Exact Algorithm for Project Scehduling with Resource Constraints Based on a New Mathematical Formulation, Technical Report 32, University of Bologna, [MO 95] S. Murer, S. Omohundro, D. Stoutamire, C. Szyperski, Iteration abstraction in Sather, working paper, International Computer Science Institute, 1947 Center St., Suite 600, Berkeley, CA 94704, USA,
212 [MT 63] J.F. Muth & G.L. Thompson, Industrial scheduling, Prentice Hall, Englewood Cliffs, NJ, [MBL 97] A. Myers, J. Bank, B. Liskov, Parametrized Types for Java, Conference record of the 24 th annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, p , ACM Press, New York, [NB 92] R. Nanda, J. Browner, Introduction to Employee Scheduling, Van Nostrand Reinhold, [NY 92] R. Nakado & T. Yamada, A Genetic Algorithm Applicable to Large Scale Job-Shop Problems, Parallel Problem Solving from Nature 2, R. Manner, B. Manderick eds., p , Elsevier Science, [Ni 96] M. Nivat, Reconstruction de polyomonios, notes manuscrites, [NS 93] E. Nowicki & C. Smutnicki, A Fast Taboo Search Algorithm for the Job-Shop Problem, Preprint 8/93 Institute of Engineering Cybernetics, Technical University of Wroclaw, [Nu 94] W. Nuijten, Time and Resource Constrainted Scheduling, a Constraint Satisfaction Approach, Ph D. Dissertation, University of Eindhoven, [Or 76] I. Or, Traveling Salesman-Type Combinatorial Problems and Their Relation to the Logistics of Regional Blood Banking, Ph D. Dissertation, Nothwestern University, [PR 91] M. Padberg, G. Rinaldi, A Branch-and-cut Algorithm for the Resolution of Large-scale Symmetric Traveling Salesman Problems. SIAM Review 33 (1), p , [PS 82] C. Papadimitriou, K. Steiglitz, Combinatorial Optimization, Prentice Hall, [Pa 97] A. Partouche, A Hybrid Method for the General Rostering Problem with a Mixed and Homogeneous WorkForce, cahiers du LAMSADE 151, Université Paris Dauphine, [Pa 84] J.H. Patterson, A Comparison of Exact Procedures for solving the Multiple Constrained Resource Project Scheduling Problem, Management Science 30 (7), p , [Pea 84] J. Pearl, Heuristics: Intelligent Search Strategies for Computer Problem Solving, Addison-Wesley, [PGPR 96] G. Pesant, M. Gendreau, J.-Y. Potvin, J.-M. Rousseau, An Exact Constraint Logic Programming Algorithm for the Travelling Salesman with Time Windows, to appear in Transportation Science, [PR 93] J.-Y. Potvin, J.-M. Rousseau, A Parallel Route Building Algorithm for the Vehicle Routing and Scheduling Problem with Time Windows, European Journal of Operational Research, 66, p , [Pr 57] R. C. Prim, Shortest Connection Networks and Some Generalizations, Bell Systems Technical Journal 36, p , [Pr 94] C. Prins, Algorithmes de graphes, Eyrolles, Paris, 1994 [Pug 94] J.-F. Puget, A C++ Implementation of CLP, Proceedings of SPICIS 94, Singapore, [PA91] J.F. Puget, P. Albert, PECOS: Programmation par Contraintes Orientée Objets, Génie Logiciel et Systèmes Experts, 23, [Ree 93] C. Reeves, Modern Heuristic Techniques for Combinatorial Problems, Halsted Press, Blackwell Scientific Publications, [Ré 94] J.C. Régin, A filtering Algorithm for constraints of difference in CSP, Proc. of AAAI'94, Seattle, p , [Ré 96] J.-C. Régin Generalized Arc Consistency for Global Cardinality Constraint, Proc. of ECAI,
213 [Rei 94] G. Reinelt, The Travelling Salesman, Computational Solutions for TSP Applications, Lecture Notes in Computer Science 840, Springer, Collection de problèmes (benchmarks) TSPLIB associée au livre disponible ftp://elib.zib-berlin.de [RL 95] S. Ridard, J.-L. Lambert, Programmer l'algorithmique par Objets, actes de Langages et Modèles à Objets (LOM'95), p , [RL 96] S. Ridard, J.-L. Lambert, Algorithmic Objects, cahiers du GREYC 7, Université de Caen, esplanade de la paix, F Caen, [RT 95] E. Rochat, E. Taillard. Probabilistic Diversification and Intensification in Local Search for Vehicle Routing, Journal of Heuristics, 1, p , [Ros 85] Roseaux, Exercices et Problèmes résolus de Recherche Opérationnelle 3, Masson, Paris, [Ru 95] R. Russell, Hybrid Heuristics for the Vehicle Routing Problem with Time Windows, Transportation Science, 29 (2), p , [Ry 63] H. Ryser, Combinatorial Mathematics, The Carus Mathematical Monographs, 14, The Mathematical Association of America, Rahway, [Sa 85] M. Savelsbergh, Local Search in Routing Problems with Time Windows, Annals of Operations Research 14, p , [Sch 86] A. Schrijver, Theory of Linear and Integer Programming, Wiley, [Sch 95] A. Schrijver, An Introduction to Combinatorial Optimization, notes de cours MMFAI, École Normale Supérieure, 45 rue d'ulm, Paris, France, [SS 94] C. Schulte, G. Smolka, Encapsulated Search for Higher-order Concurrent Constraint Programming, Logic Programming, Proceedings of the 1994 International Symposium, M. Bruynooghe ed., MIT Press, p , [SDD 86] J. Schwartz, R. Dewar, E. Dubinsky, E. Schonberg, Programming with Sets: an Introduction to SETL, Springer, New-York, [SV 97] L. Segoufin, V. Vianu, Querying spatial databases via topological invariants, soumis pour publication, [SLM 92] B. Selman, H. Levesque, D. Mitchell, A New Method for Solving Hard Satisfiability Problems Proc. of AAAI-92, p , [SB 96] N. Simonetti, E. Balas, Implementation of a Linear Time Algorithm for Certain Generalized Travelling Salesman Problems. lecture at Combinatorial Optimization (CO'96), London, [Sm 95] G. Smolka, The Oz Programming Model, Research Report RR-95-10, DFKI, Saarbrücken, juillet [So 87] M. Solomon, Algorithms for the Vehicle Routing and Scheduling Problems with Time Window Constraints, Operations Research, 35 (2), p , Collection de problèmes disponibles à l'adresse : [Shae 95] A. Shaerf, A Survey of Automated Timetabling, CWI Technical Report CS-R9567, CWI, P.O. Box 94079, 1090 GB Amsterdam, Pays Bas, [SL 94] A. Stepanov, M. Lee, The Standard Template Library, Technical Report HPL-94-34, Hewlett- Packard Laboratories, 1051 Page Mill Road, Palo Alto, CA 94304, USA, april [St 78] J.P. Stinson, A Branch and Bound Algorithm for a General Class of Multiple Resource- Constrained Scheduling Problems, unpublished Ph.D. Dissertation, North Carolina State University, Raleigh,
214 [Tai 89] E. Taillard, Parallel Taboo Search Technique for the Jobshop Scheduling Problem. Internal Report ORPWP 89/11, Ecole Polytechnique Fédérale de Lausanne, [Tai 95] E. Taillard, P. Badeau, F Gendreau, J.-Y. Potvin, A New Neighborhood Structure for the Vehicle Routing Problem with Time Windows, CRT 95-66, Université de Montréal, [TP 93] P. Thompson, H. Psaraftis, Cyclic Transfer Algorithms for Multivehicle Routing and Scheduling Problems, Operations Research 41 (5), p , [VAL94] R. Vaessens, E. Aarts & J.K. Lenstra Job Shop Scheduling by Local Search, Informs Journal on Computing 8, p , [vdv 91] S.L. van de Velde, Machine scheduling and Lagrangian relaxation. Doctoral Thesis, CWI, Amsterdam, [vh 89] P. van Hentenryck, Constraint Satisfaction in Logic Programming, The MIT Press, [vhd 91] P. van Hentenryck, Y. Deville, The Cardinality Operator: A New Logical Connective for Constraint Logic Programming, Proc. of the 8 th International Conference on Logic Programming, p , The MIT Press, Paris, [vhsd92] P. van Hentenryck, V. Saraswat, Y. Deville, Constraint Processing in cc(fd), Brown University Technical Report, [vhsd93] P. van Hentenryck, V. Saraswat, Y. Deville, The Design, Implementation, and Applications of the Constraint Language cc(fd). Brown University Technical Report, [vl 90] J. van Leeuwen, Graph Algorithms in Handbook of Theoretical Computer Science, Elsevier Science Publishers, [vl 92] P van Laarhoven, E.Aarts & J.K. Lenstra. Job Shop Scheduling by Simulated Annealing. Operations Research, 40 (1), p , [Zh 97] J. Zhou, A permutation-based approach for solving the job-shop problem, CONSTRAINTS 2 (2), p , Kluwer,
Programmation linéaire
1 Programmation linéaire 1. Le problème, un exemple. 2. Le cas b = 0 3. Théorème de dualité 4. L algorithme du simplexe 5. Problèmes équivalents 6. Complexité de l Algorithme 2 Position du problème Soit
La programmation linéaire : une introduction. Qu est-ce qu un programme linéaire? Terminologie. Écriture mathématique
La programmation linéaire : une introduction Qu est-ce qu un programme linéaire? Qu est-ce qu un programme linéaire? Exemples : allocation de ressources problème de recouvrement Hypothèses de la programmation
Cours de Master Recherche
Cours de Master Recherche Spécialité CODE : Résolution de problèmes combinatoires Christine Solnon LIRIS, UMR 5205 CNRS / Université Lyon 1 2007 Rappel du plan du cours 16 heures de cours 1 - Introduction
LE PROBLEME DU PLUS COURT CHEMIN
LE PROBLEME DU PLUS COURT CHEMIN Dans cette leçon nous définissons le modèle de plus court chemin, présentons des exemples d'application et proposons un algorithme de résolution dans le cas où les longueurs
PROBLEMES D'ORDONNANCEMENT AVEC RESSOURCES
Leçon 11 PROBLEMES D'ORDONNANCEMENT AVEC RESSOURCES Dans cette leçon, nous retrouvons le problème d ordonnancement déjà vu mais en ajoutant la prise en compte de contraintes portant sur les ressources.
Souad EL Bernoussi. Groupe d Analyse Numérique et Optimisation Rabat http ://www.fsr.ac.ma/ano/
Recherche opérationnelle Les démonstrations et les exemples seront traités en cours Souad EL Bernoussi Groupe d Analyse Numérique et Optimisation Rabat http ://www.fsr.ac.ma/ano/ Table des matières 1 Programmation
Chapitre 1 : Introduction aux bases de données
Chapitre 1 : Introduction aux bases de données Les Bases de Données occupent aujourd'hui une place de plus en plus importante dans les systèmes informatiques. Les Systèmes de Gestion de Bases de Données
Pourquoi l apprentissage?
Pourquoi l apprentissage? Les SE sont basés sur la possibilité d extraire la connaissance d un expert sous forme de règles. Dépend fortement de la capacité à extraire et formaliser ces connaissances. Apprentissage
Annexe 6. Notions d ordonnancement.
Annexe 6. Notions d ordonnancement. APP3 Optimisation Combinatoire: problèmes sur-contraints et ordonnancement. Mines-Nantes, option GIPAD, 2011-2012. [email protected] Résumé Ce document
Programmation linéaire
Programmation linéaire DIDIER MAQUIN Ecole Nationale Supérieure d Electricité et de Mécanique Institut National Polytechnique de Lorraine Mathématiques discrètes cours de 2ème année Programmation linéaire
Optimisation Discrète
Prof F Eisenbrand EPFL - DISOPT Optimisation Discrète Adrian Bock Semestre de printemps 2011 Série 7 7 avril 2011 Exercice 1 i Considérer le programme linéaire max{c T x : Ax b} avec c R n, A R m n et
Calculer avec Sage. Revision : 417 du 1 er juillet 2010
Calculer avec Sage Alexandre Casamayou Guillaume Connan Thierry Dumont Laurent Fousse François Maltey Matthias Meulien Marc Mezzarobba Clément Pernet Nicolas Thiéry Paul Zimmermann Revision : 417 du 1
Arithmétique binaire. Chapitre. 5.1 Notions. 5.1.1 Bit. 5.1.2 Mot
Chapitre 5 Arithmétique binaire L es codes sont manipulés au quotidien sans qu on s en rende compte, et leur compréhension est quasi instinctive. Le seul fait de lire fait appel au codage alphabétique,
Date : 18.11.2013 Tangram en carré page
Date : 18.11.2013 Tangram en carré page Titre : Tangram en carré Numéro de la dernière page : 14 Degrés : 1 e 4 e du Collège Durée : 90 minutes Résumé : Le jeu de Tangram (appelé en chinois les sept planches
LES CARTES À POINTS : POUR UNE MEILLEURE PERCEPTION
LES CARTES À POINTS : POUR UNE MEILLEURE PERCEPTION DES NOMBRES par Jean-Luc BREGEON professeur formateur à l IUFM d Auvergne LE PROBLÈME DE LA REPRÉSENTATION DES NOMBRES On ne conçoit pas un premier enseignement
INTRODUCTION À L ANALYSE FACTORIELLE DES CORRESPONDANCES
INTRODUCTION À L ANALYSE FACTORIELLE DES CORRESPONDANCES Dominique LAFFLY Maître de Conférences, Université de Pau Laboratoire Société Environnement Territoire UMR 5603 du CNRS et Université de Pau Domaine
Introduction. I Étude rapide du réseau - Apprentissage. II Application à la reconnaissance des notes.
Introduction L'objectif de mon TIPE est la reconnaissance de sons ou de notes de musique à l'aide d'un réseau de neurones. Ce réseau doit être capable d'apprendre à distinguer les exemples présentés puis
Programmation Par Contraintes
Programmation Par Contraintes Cours 2 - Arc-Consistance et autres amusettes David Savourey CNRS, École Polytechnique Séance 2 inspiré des cours de Philippe Baptiste, Ruslan Sadykov et de la thèse d Hadrien
O b s e r v a t o i r e E V A P M. Taxonomie R. Gras - développée
O b s e r v a t o i r e E V A P M É q u i p e d e R e c h e r c h e a s s o c i é e à l ' I N R P Taxonomie R. Gras - développée Grille d'analyse des objectifs du domaine mathématique et de leurs relations
2. RAPPEL DES TECHNIQUES DE CALCUL DANS R
2. RAPPEL DES TECHNIQUES DE CALCUL DANS R Dans la mesure où les résultats de ce chapitre devraient normalement être bien connus, il n'est rappelé que les formules les plus intéressantes; les justications
Cours 02 : Problème général de la programmation linéaire
Cours 02 : Problème général de la programmation linéaire Cours 02 : Problème général de la Programmation Linéaire. 5 . Introduction Un programme linéaire s'écrit sous la forme suivante. MinZ(ou maxw) =
Correction de l examen de la première session
de l examen de la première session Julian Tugaut, Franck Licini, Didier Vincent Si vous trouvez des erreurs de Français ou de mathématiques ou bien si vous avez des questions et/ou des suggestions, envoyez-moi
Grandes lignes ASTRÉE. Logiciels critiques. Outils de certification classiques. Inspection manuelle. Definition. Test
Grandes lignes Analyseur Statique de logiciels Temps RÉel Embarqués École Polytechnique École Normale Supérieure Mercredi 18 juillet 2005 1 Présentation d 2 Cadre théorique de l interprétation abstraite
Cours de Recherche Opérationnelle IUT d Orsay. Nicolas M. THIÉRY. E-mail address: [email protected] URL: http://nicolas.thiery.
Cours de Recherche Opérationnelle IUT d Orsay Nicolas M. THIÉRY E-mail address: [email protected] URL: http://nicolas.thiery.name/ CHAPTER 1 Introduction à l optimisation 1.1. TD: Ordonnancement
Rapport d'analyse des besoins
Projet ANR 2011 - BR4CP (Business Recommendation for Configurable products) Rapport d'analyse des besoins Janvier 2013 Rapport IRIT/RR--2013-17 FR Redacteur : 0. Lhomme Introduction...4 La configuration
Fonctions de plusieurs variables
Module : Analyse 03 Chapitre 00 : Fonctions de plusieurs variables Généralités et Rappels des notions topologiques dans : Qu est- ce que?: Mathématiquement, n étant un entier non nul, on définit comme
CONCEPTION Support de cours n 3 DE BASES DE DONNEES
CONCEPTION Support de cours n 3 DE BASES DE DONNEES Auteur: Raymonde RICHARD PRCE UBO PARTIE III. - LA DESCRIPTION LOGIQUE ET PHYSIQUE DES DONNEES... 2 A. Les concepts du modèle relationnel de données...
Communications collectives et ordonnancement en régime permanent pour plates-formes hétérogènes
Loris MARCHAL Laboratoire de l Informatique du Parallélisme Équipe Graal Communications collectives et ordonnancement en régime permanent pour plates-formes hétérogènes Thèse réalisée sous la direction
modélisation solide et dessin technique
CHAPITRE 1 modélisation solide et dessin technique Les sciences graphiques regroupent un ensemble de techniques graphiques utilisées quotidiennement par les ingénieurs pour exprimer des idées, concevoir
Problèmes d ordonnancement dans les systèmes de production. Journée Automatique et Optimisation Université de Paris 12 20 Mars 2003
Problèmes d ordonnancement dans les systèmes de production Michel Gourgand Université Blaise Pascal Clermont Ferrand LIMOS CNRS UMR 6158 1 Le LIMOS Laboratoire d Informatique, de Modélisation et d Optimisation
TP n 2 Concepts de la programmation Objets Master 1 mention IL, semestre 2 Le type Abstrait Pile
TP n 2 Concepts de la programmation Objets Master 1 mention IL, semestre 2 Le type Abstrait Pile Dans ce TP, vous apprendrez à définir le type abstrait Pile, à le programmer en Java à l aide d une interface
Algorithmes de recherche
Algorithmes de recherche 1 Résolution de problèmes par recherche On représente un problème par un espace d'états (arbre/graphe). Chaque état est une conguration possible du problème. Résoudre le problème
Exemples de problèmes et d applications. INF6953 Exemples de problèmes 1
Exemples de problèmes et d applications INF6953 Exemples de problèmes Sommaire Quelques domaines d application Quelques problèmes réels Allocation de fréquences dans les réseaux radio-mobiles Affectation
Catalogue des connaissances de base en mathématiques dispensées dans les gymnases, lycées et collèges romands.
Catalogue des connaissances de base en mathématiques dispensées dans les gymnases, lycées et collèges romands. Pourquoi un autre catalogue en Suisse romande Historique En 1990, la CRUS (Conférences des
Théorème du point fixe - Théorème de l inversion locale
Chapitre 7 Théorème du point fixe - Théorème de l inversion locale Dans ce chapitre et le suivant, on montre deux applications importantes de la notion de différentiabilité : le théorème de l inversion
2 Grad Info Soir Langage C++ Juin 2007. Projet BANQUE
2 Grad Info Soir Langage C++ Juin 2007 Projet BANQUE 1. Explications L'examen comprend un projet à réaliser à domicile et à documenter : - structure des données, - objets utilisés, - relations de dépendance
Info0804. Cours 6. Optimisation combinatoire : Applications et compléments
Recherche Opérationnelle Optimisation combinatoire : Applications et compléments Pierre Delisle Université de Reims Champagne-Ardenne Département de Mathématiques et Informatique 17 février 2014 Plan de
Exercices du Cours de la programmation linéaire donné par le Dr. Ali DERBALA
75. Un plombier connaît la disposition de trois tuyaux sous des dalles ( voir figure ci dessous ) et il lui suffit de découvrir une partie de chacun d eux pour pouvoir y poser les robinets. Il cherche
CHAPITRE VIII : Les circuits avec résistances ohmiques
CHAPITRE VIII : Les circuits avec résistances ohmiques VIII. 1 Ce chapitre porte sur les courants et les différences de potentiel dans les circuits. VIII.1 : Les résistances en série et en parallèle On
Brique BDL Gestion de Projet Logiciel
Brique BDL Gestion de Projet Logiciel Processus de développement pratiqué à l'enst [email protected] url:http://www.infres.enst.fr/~vignes/bdl Poly: Computer elective project F.Gasperoni Brique BDL
Les diagrammes de modélisation
L approche Orientée Objet et UML 1 Plan du cours Introduction au Génie Logiciel L approche Orientée Objet et Notation UML Les diagrammes de modélisation Relations entre les différents diagrammes De l analyse
COMMUNICATEUR BLISS COMMANDE PAR UN SENSEUR DE POSITION DE L'OEIL
COMMUNICATEUR BLISS COMMANDE PAR UN SENSEUR DE POSITION DE L'OEIL J. TICHON(1) (2), J.-M. TOULOTTE(1), G. TREHOU (1), H. DE ROP (2) 1. INTRODUCTION Notre objectif est de réaliser des systèmes de communication
ANNEXE 1 RÈGLEMENTS GÉNÉRAUX 1 ASSOCIATION DES ARCHÉOLOGUES PROFESSIONNELS DU QUÉBEC (AAQ) CODE D ÉTHIQUE ET DES NORMES PROFESSIONNELLES
ANNEXE 1 RÈGLEMENTS GÉNÉRAUX 1 ASSOCIATION DES ARCHÉOLOGUES PROFESSIONNELS DU QUÉBEC (AAQ) CODE D ÉTHIQUE ET DES NORMES PROFESSIONNELLES Ce code fait partie intégrante du règlement de l'association des
Annexe sur la maîtrise de la qualité
Version du 09/07/08 Annexe sur la maîtrise de la qualité La présente annexe précise les modalités d'application, en matière de maîtrise de la qualité, de la circulaire du 7 janvier 2008 fixant les modalités
MPI Activité.10 : Logique binaire Portes logiques
MPI Activité.10 : Logique binaire Portes logiques I. Introduction De nombreux domaines font appel aux circuits logiques de commutation : non seulement l'informatique, mais aussi les technologies de l'asservissement
Le modèle de données
Le modèle de données Introduction : Une fois que l étude des besoins est complétée, deux points importants sont à retenir : Les données du système étudié Les traitements effectués par le système documentaire.
chapitre 4 Nombres de Catalan
chapitre 4 Nombres de Catalan I Dénitions Dénition 1 La suite de Catalan (C n ) n est la suite dénie par C 0 = 1 et, pour tout n N, C n+1 = C k C n k. Exemple 2 On trouve rapidement C 0 = 1, C 1 = 1, C
Optimisation non linéaire Irène Charon, Olivier Hudry École nationale supérieure des télécommunications
Optimisation non linéaire Irène Charon, Olivier Hudry École nationale supérieure des télécommunications A. Optimisation sans contrainte.... Généralités.... Condition nécessaire et condition suffisante
Exercices Alternatifs. Quelqu un aurait-il vu passer un polynôme?
Exercices Alternatifs Quelqu un aurait-il vu passer un polynôme? c 2004 Frédéric Le Roux, François Béguin (copyleft LDL : Licence pour Documents Libres). Sources et figures: polynome-lagrange/. Version
Exercices Alternatifs. Quelqu un aurait-il vu passer un polynôme?
Exercices Alternatifs Quelqu un aurait-il vu passer un polynôme? c 2004 Frédéric Le Roux, François Béguin (copyleft LDL : Licence pour Documents Libres). Sources et figures: polynome-lagrange/. Version
basée sur le cours de Bertrand Legal, maître de conférences à l ENSEIRB www.enseirb.fr/~legal Olivier Augereau Formation UML
basée sur le cours de Bertrand Legal, maître de conférences à l ENSEIRB www.enseirb.fr/~legal Olivier Augereau Formation UML http://olivier-augereau.com Sommaire Introduction I) Les bases II) Les diagrammes
L apprentissage automatique
L apprentissage automatique L apprentissage automatique L'apprentissage automatique fait référence au développement, à l analyse et à l implémentation de méthodes qui permettent à une machine d évoluer
Chapitre 2 Le problème de l unicité des solutions
Université Joseph Fourier UE MAT 127 Mathématiques année 2011-2012 Chapitre 2 Le problème de l unicité des solutions Ce que nous verrons dans ce chapitre : un exemple d équation différentielle y = f(y)
Systèmes de transport public guidés urbains de personnes
service technique des Remontées mécaniques et des Transports guidés Systèmes de transport public guidés urbains de personnes Principe «GAME» (Globalement Au Moins Équivalent) Méthodologie de démonstration
Fiche méthodologique Rédiger un cahier des charges
Fiche méthodologique Rédiger un cahier des charges Plan de la fiche : 1 : Présentation de la fiche 2 : Introduction : les grands principes 3 : Contenu, 1 : positionnement et objectifs du projet 4 : Contenu,
Baccalauréat technologique
Baccalauréat technologique Épreuve relative aux enseignements technologiques transversaux, épreuve de projet en enseignement spécifique à la spécialité et épreuve d'enseignement technologique en langue
Jade. Projet Intelligence Artificielle «Devine à quoi je pense»
Jade Projet Intelligence Artificielle «Devine à quoi je pense» Réalisé par Djénéba Djikiné, Alexandre Bernard et Julien Lafont EPSI CSII2-2011 TABLE DES MATIÈRES 1. Analyse du besoin a. Cahier des charges
Logique binaire. Aujourd'hui, l'algèbre de Boole trouve de nombreuses applications en informatique et dans la conception des circuits électroniques.
Logique binaire I. L'algèbre de Boole L'algèbre de Boole est la partie des mathématiques, de la logique et de l'électronique qui s'intéresse aux opérations et aux fonctions sur les variables logiques.
4.2 Unités d enseignement du M1
88 CHAPITRE 4. DESCRIPTION DES UNITÉS D ENSEIGNEMENT 4.2 Unités d enseignement du M1 Tous les cours sont de 6 ECTS. Modélisation, optimisation et complexité des algorithmes (code RCP106) Objectif : Présenter
Stages 2014-2015 ISOFT : UNE SOCIETE INNOVANTE. Contact : Mme Lapedra, [email protected]
Stages 2014-2015 ISOFT : UNE SOCIETE INNOVANTE Contact : Mme Lapedra, [email protected] ISoft, éditeur de logiciels, est spécialisé dans l informatique décisionnelle et l analyse de données. Son expertise
Ordonnancement. N: nains de jardin. X: peinture extérieure. E: électricité T: toit. M: murs. F: fondations CHAPTER 1
CHAPTER 1 Ordonnancement 1.1. Étude de cas Ordonnancement de tâches avec contraintes de précédences 1.1.1. Exemple : construction d'une maison. Exercice. On veut construire une maison, ce qui consiste
Exo7. Matrice d une application linéaire. Corrections d Arnaud Bodin.
Exo7 Matrice d une application linéaire Corrections d Arnaud odin. Exercice Soit R muni de la base canonique = ( i, j). Soit f : R R la projection sur l axe des abscisses R i parallèlement à R( i + j).
Guide No.2 de la Recommandation Rec (2009).. du Comité des Ministres aux États membres sur la démocratie électronique
DIRECTION GENERALE DES AFFAIRES POLITIQUES DIRECTION DES INSTITUTIONS DEMOCRATIQUES Projet «BONNE GOUVERNANCE DANS LA SOCIETE DE L INFORMATION» CAHDE (2009) 2F Strasbourg, 20 janvier 2009 Guide No.2 de
Rappels sur les suites - Algorithme
DERNIÈRE IMPRESSION LE 14 septembre 2015 à 12:36 Rappels sur les suites - Algorithme Table des matières 1 Suite : généralités 2 1.1 Déition................................. 2 1.2 Exemples de suites............................
GESTION DE PROJET SÉANCE 2 : LES CYCLE DE VIE D'UN PROJET
GESTION DE PROJET SÉANCE 2 : LES CYCLE DE VIE D'UN PROJET 1 Tianxiao LIU Licence Professionnelle Réseaux & Sécurité Université de Cergy-Pontoise http://depinfo.u-cergy.fr/~tliu/lpg.php PLAN Objectif et
LES MÉTHODES DE POINT INTÉRIEUR 1
Chapitre XIII LES MÉTHODES DE POINT INTÉRIEUR 1 XIII.1 Introduction Nous débutons par un rappel de la formulation standard d un problème d optimisation 2 linéaire et donnons un bref aperçu des différences
Programmation Linéaire - Cours 1
Programmation Linéaire - Cours 1 P. Pesneau [email protected] Université Bordeaux 1 Bât A33 - Bur 265 Ouvrages de référence V. Chvátal - Linear Programming, W.H.Freeman, New York, 1983.
Reconstruction de bâtiments en 3D à partir de nuages de points LIDAR
Reconstruction de bâtiments en 3D à partir de nuages de points LIDAR Mickaël Bergem 25 juin 2014 Maillages et applications 1 Table des matières Introduction 3 1 La modélisation numérique de milieux urbains
Calcul matriciel. Définition 1 Une matrice de format (m,n) est un tableau rectangulaire de mn éléments, rangés en m lignes et n colonnes.
1 Définitions, notations Calcul matriciel Définition 1 Une matrice de format (m,n) est un tableau rectangulaire de mn éléments, rangés en m lignes et n colonnes. On utilise aussi la notation m n pour le
ÉdIteur officiel et fournisseur de ServIceS professionnels du LogIcIeL open Source ScILab
ÉdIteur officiel et fournisseur de ServIceS professionnels du LogIcIeL open Source ScILab notre compétence d'éditeur à votre service créée en juin 2010, Scilab enterprises propose services et support autour
Service de réplication des données HP pour la gamme de disques Continuous Access P9000 XP
Service de réplication des données HP pour la gamme de disques Continuous Access P9000 XP Services HP Care Pack Données techniques Le service de réplication des données HP pour Continuous Access offre
Axiomatique de N, construction de Z
Axiomatique de N, construction de Z Table des matières 1 Axiomatique de N 2 1.1 Axiomatique ordinale.................................. 2 1.2 Propriété fondamentale : Le principe de récurrence.................
Analyse tarifaire en ligne (TAO) de l'omc
Analyse tarifaire en ligne (TAO) de l'omc L'analyse tarifaire en ligne (TAO) permet d'effectuer des recherches et d'analyser les données tarifaires conservées dans deux bases de données de l'omc, à savoir
Nouvelles propositions pour la résolution exacte du sac à dos multi-objectif unidimensionnel en variables binaires
Nouvelles propositions pour la résolution exacte du sac à dos multi-objectif unidimensionnel en variables binaires Julien Jorge [email protected] Laboratoire d Informatique de Nantes Atlantique,
OASIS www.oasis-open.org/committees/xacml/docs/docs.shtml Date de publication
Statut du Committee Working Draft document Titre XACML Language Proposal, version 0.8 (XACML : XML Access Control Markup Language) Langage de balisage du contrôle d'accès Mot clé Attestation et sécurité
FICHE UE Licence/Master Sciences, Technologies, Santé Mention Informatique
NOM DE L'UE : Algorithmique et programmation C++ LICENCE INFORMATIQUE Non Alt Alt S1 S2 S3 S4 S5 S6 Parcours : IL (Ingénierie Logicielle) SRI (Systèmes et Réseaux Informatiques) MASTER INFORMATIQUE Non
Raisonnement par récurrence Suites numériques
Chapitre 1 Raisonnement par récurrence Suites numériques Terminale S Ce que dit le programme : CONTENUS CAPACITÉS ATTENDUES COMMENTAIRES Raisonnement par récurrence. Limite finie ou infinie d une suite.
Utilisation des médicaments au niveau des soins primaires dans les pays en développement et en transition
09-0749 1 WHO/EMP/MAR/2009.3 Utilisation des médicaments au niveau des soins primaires dans les pays en développement et en transition Synthèse des résultats des études publiées entre 1990 et 2006 Organisation
LES OUTILS D ALIMENTATION DU REFERENTIEL DE DB-MAIN
LES OUTILS D ALIMENTATION DU REFERENTIEL DE DB-MAIN Les contenues de ce document sont la propriété exclusive de la société REVER. Ils ne sont transmis qu à titre d information et ne peuvent en aucun cas
Installation de Windows 2003 Serveur
Installation de Windows 2003 Serveur Introduction Ce document n'explique pas les concepts, il se contente de décrire, avec copies d'écran, la méthode que j'utilise habituellement pour installer un Windows
Synthèse «Le Plus Grand Produit»
Introduction et Objectifs Synthèse «Le Plus Grand Produit» Le document suivant est extrait d un ensemble de ressources plus vastes construites par un groupe de recherche INRP-IREM-IUFM-LEPS. La problématique
Probabilités sur un univers fini
[http://mp.cpgedupuydelome.fr] édité le 7 août 204 Enoncés Probabilités sur un univers fini Evènements et langage ensembliste A quelle condition sur (a, b, c, d) ]0, [ 4 existe-t-il une probabilité P sur
COURS EULER: PROGRAMME DE LA PREMIÈRE ANNÉE
COURS EULER: PROGRAMME DE LA PREMIÈRE ANNÉE Le cours de la première année concerne les sujets de 9ème et 10ème années scolaires. Il y a bien sûr des différences puisque nous commençons par exemple par
Université de Lorraine Licence AES LIVRET DE STAGE LICENCE 2014-2015
Université de Lorraine Licence AES LIVRET DE STAGE LICENCE 2014-2015 1 LA REDACTION DU RAPPORT DE STAGE Le mémoire ne doit pas consister à reprendre tels quels des documents internes de l entreprise ou
UNITE U 6.2 : PROJET TECHNIQUE OBJET DE L'EPREUVE.
UNITE U 6.2 : PROJET TECHNIQUE OBJET DE L'EPREUVE. Cette épreuve permet de valider les compétences C1, C2, C3 et T2 du référentiel au travers de la démarche de projet 15 que le candidat aura mis en œuvre.
http://cri.univ-lille1.fr Virtualisation de Windows dans Ubuntu Linux
http://cri.univ-lille1.fr Virtualisation de Windows dans Ubuntu Linux Version 1.0 Septembre 2011 SOMMAIRE 1. Introduction 3 2. Installation du logiciel de virtualisation VirtualBox 4 3. Création d'une
Intelligence Artificielle et Systèmes Multi-Agents. Badr Benmammar [email protected]
Intelligence Artificielle et Systèmes Multi-Agents Badr Benmammar [email protected] Plan La première partie : L intelligence artificielle (IA) Définition de l intelligence artificielle (IA) Domaines
Championnat de France de Grilles Logiques Finale 7 juin 2014. Livret d'instructions
Championnat de France de Grilles Logiques Finale 7 juin 0 Livret d'instructions Épreuve Thème Horaires Durée Points Déjà vu? h h minutes 0 Medley international h h 0 minutes 00 Futur proche? h h0 minutes
IFT1215 Introduction aux systèmes informatiques
Introduction aux circuits logiques de base IFT25 Architecture en couches Niveau 5 Niveau 4 Niveau 3 Niveau 2 Niveau Niveau Couche des langages d application Traduction (compilateur) Couche du langage d
Projet de traitement d'image - SI 381 reconstitution 3D d'intérieur à partir de photographies
Projet de traitement d'image - SI 381 reconstitution 3D d'intérieur à partir de photographies Régis Boulet Charlie Demené Alexis Guyot Balthazar Neveu Guillaume Tartavel Sommaire Sommaire... 1 Structure
De même, le périmètre P d un cercle de rayon 1 vaut P = 2π (par définition de π). Mais, on peut démontrer (difficilement!) que
Introduction. On suppose connus les ensembles N (des entiers naturels), Z des entiers relatifs et Q (des nombres rationnels). On s est rendu compte, depuis l antiquité, que l on ne peut pas tout mesurer
1. Introduction...2. 2. Création d'une requête...2
1. Introduction...2 2. Création d'une requête...2 3. Définition des critères de sélection...5 3.1 Opérateurs...5 3.2 Les Fonctions...6 3.3 Plusieurs critères portant sur des champs différents...7 3.4 Requête
2. Activités et Modèles de développement en Génie Logiciel
2. Activités et Modèles de développement en Génie Logiciel Bernard ESPINASSE Professeur à l'université d'aix-marseille Plan Les Activités du GL Analyse des besoins Spécification globale Conceptions architecturale
Généralités sur le Langage Java et éléments syntaxiques.
Généralités sur le Langage Java et éléments syntaxiques. Généralités sur le Langage Java et éléments syntaxiques....1 Introduction...1 Genéralité sur le langage Java....1 Syntaxe de base du Langage...
Installation de Windows 2000 Serveur
Installation de Windows 2000 Serveur Introduction Ce document n'explique pas les concepts, il se contente de décrire, avec copies d'écran, la méthode que j'utilise habituellement pour installer un Windows
Université de Bangui. Modélisons en UML
Université de Bangui CRM Modélisons en UML Ce cours a été possible grâce à l initiative d Apollinaire MOLAYE qui m a contacté pour vous faire bénéficier de mes connaissances en nouvelles technologies et
«Manuel Pratique» Gestion budgétaire
11/06/01 B50/v2.31/F/MP005.01 «Manuel Pratique» Gestion budgétaire Finance A l usage des utilisateurs de Sage BOB 50 Solution Sage BOB 50 2 L éditeur veille à la fiabilité des informations publiées, lesquelles
Anticiper pour avoir une innovation d'avance : le leitmotiv de Pierre Jouniaux, entrepreneur du big data!
Anticiper pour avoir une innovation d'avance : le leitmotiv de Pierre Jouniaux, entrepreneur du big data! Pierre Jouniaux http://www.safety line.fr CV : Pierre Jouniaux, ingénieur aéronautique, pilote
