Le test de satisfiabilité en logique Booléenne
Transcription
Le test de satisfiabilité en logique Booléenne
Université Paris 8 - [email protected] Le test de satisfiabilité en logique Booléenne Daniel Goossens Décembre 2008 Université Paris 8 Département Informatique UFR MITSIC Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation ; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". 1 c 2008 D. Goossens Université Paris 8 - [email protected] TABLE DES MATIÈRES Table des matières 1 Introduction 3 2 Les 2.1 2.2 2.3 2.4 2.5 problèmes à contraintes Le test de satisfiabilité . . . . . . . . . . . . . . . . . . . . . La résolution de problèmes combinatoires . . . . . . . . . . Un modèle minimal des problèmes à contraintes . . . . . . . l’encodage des problèmes à contraintes . . . . . . . . . . . . Le problème des n reines . . . . . . . . . . . . . . . . . . . . 2.5.1 Résolution algorithmique . . . . . . . . . . . . . . . 2.5.2 Résolution par un raisonnement automatique général 2.5.3 Encodage en cnf du problème des 3 reines . . . . . . 2.5.4 Génération automatique de l’encodage . . . . . . . . 2.5.5 Résolution avec un SAT-solver . . . . . . . . . . . . 2.6 Un formalisme intermédiaire : les CSP . . . . . . . . . . . . 2.7 Les cryptogrammes . . . . . . . . . . . . . . . . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 4 7 9 9 11 11 12 15 18 20 c 2008 D. Goossens Université Paris 8 - [email protected] Le test de satisfiabilité en logique Booléenne 1 Introduction Quand on se donne l’objectif de modéliser sur ordinateur des mécanismes aussi ambitieux que la perception, la mémoire, le raisonnement, la compréhension, le langage, on tombe obligatoirement sur des problèmes complexes dont la solution ne peut se réduire à quelques algorithmes connus. A la différence de l’algorithmique, l’intelligence artificielle s’intéresse à des méthodes de résolution de problèmes qu’on ne sait pas encore réduire à des algorithmes complets et efficaces. Les méthodes de l’intelligence artificielle, quand elles sont efficaces, sont souvent incomplètes. Quand elles se veulent complètes et efficaces, il est toujours possible de les pièger avec des exemples où elles échouent à donner une réponse en un temps raisonnable. Ca n’empêche pas qu’elles soient utiles, tant qu’on ne sait pas mieux faire. Ce cours d’intelligence artificielle spécialisé dans le raisonnement automatique est complémentaire de cours sur d’autres méthodes aux mêmes objectifs, comme les réseaux de neurones, la programmation génétique ou la logique floue. Il présente des situations inaccessibles à ces autres méthodes. De la même façon, les cours sur les réseaux de neurones, la programmation génétique ou la logique floue s’attacheront à présenter des exemples que le raisonnement automatique, basé sur la logique classique, ne sait pas gérer. L’objectif premier de ce cours est de vous rendre capable de représenter des problèmes combinatoires dans un langage que savent lire les outils efficaces de raisonnement automatique actuellement disponibles, et d’utiliser ces outils pour résoudre les problèmes ainsi représentés. Le cours présuppose de savoir programmer en C et d’avoir des notions d’algèbre de Boole ou de logique des propositions. Le travail demandé consiste en des exercices, qui peuvent être résolus en peu de temps, et des projets au choix, selon l’investissement et la note que vous envisagez. 2 2.1 Les problèmes à contraintes Le test de satisfiabilité L’opération de base du raisonnement automatique est le test de satisfiabilité d’une expression logique. L’expression logique encode un problème combinatoire à résoudre. Elle impose un ensemble de contraintes sur l’ensemble de ses variables. Seules certaines combinaisons de valeurs pourront, en instanciant ces variables, satisfaire les contraintes. Ce sont les solutions du problème. Dans le cas où aucune combinaison de valeurs ne peut satisfaire 3 c 2008 D. Goossens Université Paris 8 - [email protected] 2.2 La résolution de problèmes combinatoires les contraintes, le problème n’a pas de solution. On dit que l’expression logique qui l’encode est insatisfiable. Dans le cas où la mémoire des faits est finie, les autres opérations du raisonnement, comme la recherche d’implications ou d’équivalences, peuvent être programmées en utilisant le test de satisfiabilité. 2.2 La résolution de problèmes combinatoires La classe des problèmes combinatoires comprend ceux dont les solutions appartiennent à un ensemble dont les éléments peuvent être énumérés. Cet ensemble s’appelle l’espace de recherche des solutions du problème. L’analyse logico-mathématique du problème a pour objectif de cerner un espace de recherche aussi petit que possible, en démontrant qu’il contient une solution. Ensuite, on peut par exemple explorer exhaustivement l’espace de recherche ainsi cerné avec un algorithme systématique. Si l’espace de recherche est fini, ça garantit qu’une solution sera trouvée, mais ça ne dit pas en combien de temps. On peut alors utiliser des heuristiques pour ignorer avec un minimum de risques de grandes portions de l’espace de recherche ou on peut abandonner l’exploration exhaustive au profit d’explorations locales autour d’ébauches de solutions ou encore utiliser des méthodes d’exploration inspirées de mécanismes Darwiniens, comme la programmation génétique. 2.3 Un modèle minimal des problèmes à contraintes Le test de satisfiabilité peut être modélisé très simplement. Soit une mémoire dont les cases sont des bits, chacune pouvant prendre une valeur parmi {0, 1}. Ces bits sont les variables de l’expression logique (ici, ce sont des variables booléennes). On ajoute à cette mémoire des contraintes. Chaque contrainte associe à un sous-ensemble de n cases une ou plusieurs chaînes de n bits interdites. La conjonction des contraintes est l’expression logique qui encode le problème. Les solutions au problème ainsi encodé sont les chaînes de bits qui remplissent la mémoire sans violer de contrainte (voir les figures 1 et 2). Pour vérifier si un problème de décision ainsi encodé admet au moins une solution, c’est à dire pour vérifier si l’ensemble des contraintes est satisfiable, il suffit d’énumérer les 2n valuations possibles des n bits de sa mémoire et de vérifier pour chacune si toutes les contraintes sont satisfaites. Nous verrons dans la suite du cours que ce modèle rudimentaire permet cependant d’encoder une importante classe de problèmes complexes. 4 c 2008 D. Goossens Université Paris 8 - [email protected] 010 100 011 2.3 Un modèle minimal des problèmes à contraintes 111 000 101 000 110 1001 0110 0111 0011 1111 Fig. 1 – Chaque rectangle connecté à des cases de la mémoire contient des valeurs interdites. Le rectangle qui contient la valeur 011, par exemple, interdit que les 3 cases auxquelles il est connecté contiennent dans cet ordre les valeurs 0, 1 et 1. Mais elles peuvent contenir 1 0 1, par exemple. Il s’agit de trouver une chaîne de bits à ranger dans toute la mémoire de façon à ne violer aucune des contraintes. Dans cet exemple, la mémoire contient 13 cases. La chaîne 1111111111111 viole deux contraintes. La chaîne 1111111111101 ne viole aucune contrainte. 00 11 00 11 00 11 Fig. 2 – Cet exemple est insatisfiable. On peut vérifier qu’aucune chaîne de trois bits ne peut satisfaire les contraintes. Exercice 1. Que font les fonctions explorer1 et explorer2 de la figure 3 ? quelle différence entre les deux méthodes ? Le deux fonctions printbits et printtab ne sont pas fournies. Programmez les pour qu’elles impriment respectivement les n premiers bits de mem et les n premières cases de tab. Détails techniques : Un prérequis à ce cours est la capacité à programmer en C. Si vous n’avez pas appris la programmation sur les bits (opérateurs "bitwise" «, », &, |, ~), étudiez 5 c 2008 D. Goossens Université Paris 8 - [email protected] unsigned long long mem = 0; 2.3 Un modèle minimal des problèmes à contraintes // max 64 bits void explorer1 (unsigned int n) while (mem < (((unsigned long long) 1) << printbits(mem, n); mem++; } } n)) { appel : explorer1(25), puis explorer1(30), etc. int tab[n]; void explorer2 (unsigned int i) { if (i < n) { tab[i] = 0; explorer2(i+1); tab[i] = 1; explorer2(i+1); } else { printtab(tab, n); } } appel : explorer2(0) Fig. 3 – Deux fonctions explorer1 et explorer2. le chapitre 7, page 82, du cours de programmation en C de Jym Feat, ou le livre "Le langage C" de Kernighan et Ritchie, ou téléchargez un tutoriel sur Internet. Pour avoir la taille du type "unsigned long long" sur votre machine, imprimez la valeur de l’expression sizeof(unsigned long long). Si c’est 8, ça fait 8 octets, donc 8*8=64 bits (voir l’exercice cx10.1 du cours de Jym Feat). Exercice 2. Choisir une série de valeurs pour n, mettons à partir de n = 10, puis 11, 12, 13, 14 ... Ecrire une boucle qui mesure le temps de calcul des fonctions explorer1 et explorer2 où vous aurez retiré les appels de printbits et printtab, pour chaque valeur de n. Editez le graphique qui fait correspondre chaque valeur de n au temps de calcul de explorer1 6 c 2008 D. Goossens Université Paris 8 - [email protected] 2.4 l’encodage des problèmes à contraintes et à celui de explorer2. Déterminez la valeur minimale de n pour laquelle le temps dépasse la minute sur votre machine. Pour produire cette courbe de correspondance entre les tailles et les temps, vous pouvez utiliser n’importe quel tableur genre Excel, mais vous pouvez aussi utiliser l’application gratuite gnuplot (http ://www.gnuplot.info/). Pour mesurer les temps de calcul, utilisez la fonction times (tapez "man times" sur votre terminal et consultez le cours 412, "éléments de systèmes d’exploitation"). Sauvegardez dans un fichier et passez le fichier à gnuplot. Si vous n’aimez pas l’informatique, vous pouvez aussi dessiner la courbe à la main sur une feuille de papier millimétré. Quel genre de courbe obtient-on ? linéaire ? polynômiale ? exponentielle ? Quel temps de calcul peut-on prévoir pour n = 50 ? et pour n = 51 ? Ce dernier exercice donne une idée de la taille maximum en nombre de bits des problèmes de décision qu’il est envisageable de résoudre avec ces méthodes exhaustives. La suite du cours a comme objectif d’apprendre à utiliser des outils open source disponibles sur Internet pour résoudre en des temps raisonnables des problèmes codables sur 100 bits, 1000 bits, et jusqu’à plusieurs centaines de milliers de bits. 2.4 l’encodage des problèmes à contraintes Commençons par le problème de la figure 2. Comme il ne nécessite que 3 variables, les deux méthodes explorer1 ou explorer2 suffisent pour le résoudre. Exercice 3 (facultatif). Décrire avec des structures de données C le tableau de 3 bits et les 3 contraintes du problème de la figure 2. Choisissez une des deux fonctions explorer1 ou explorer2. Remplacez la fonction printbits ou printtab par une fonction qui vérifie si la solution courante (le contenu de mem ou de tab) satisfait les 3 contraintes et qui dans ce cas imprime les 3 bits. Vérifiez ainsi que le problème de la figure 2 n’a pas de solution. Comme on le voit, le langage C n’est pas pratique pour encoder des contraintes Booléennes sur des variables Booléennes. On va donc utiliser un langage spécialement adapté, celui de l’algèbre Booléenne. Les expressions Booléennes en forme normale conjonctive (fnc en français, cnf pour Conjunctive Normal Form en anglais) sont le format d’entrée des algorithmes de raisonnement Booléen et de leurs jeux de tests (benchmarks) téléchargeables sur Internet. On appelle variables Booléennes les bits des schémas comme ceux des figures 1 et 2. Nommons a b c les trois variables Booléennes du schéma de la figure 2. La première contrainte interdit les valuations (a = 0 et b = 0) et (a = 1 et b = 1). En algèbre de Boole, on note ¬ la négation, ∧ la conjonction et ∨ la disjonction. La valuation a = 1 s’écrit a et la valuation a = 0 s’écrit ¬a, qu’on abrévie à ā. La première contrainte s’écrit ainsi : ¬(ā ∧ b̄) ∧ ¬(a ∧ b). Les lois de Demorgan et la simplification de la double négation : 7 c 2008 D. Goossens Université Paris 8 - [email protected] 2.4 l’encodage des problèmes à contraintes ¬(e ∧ f ) = ē ∨ f¯ ¬(e ∨ f ) = ē ∧ f¯ ¬¬e = e où e et f sont des expressions Booléennes quelconques, permettent de réécrire la première contrainte sous cette forme : (a ∨ b) ∧ (ā ∨ b̄) (1) Cette expression Booléenne est dite en forme normale conjonctive, qu’on abréviera au sigle anglais cnf. Une cnf est ainsi toujours une conjonction de clauses. Chaque clause d’une cnf est une disjonction de littéraux. Chaque littéral d’une clause est soit une variable Booléenne, comme a, soit sa négation ā. Dans la notation des cnfs, on omet les symboles de disjonction et de conjonction. La conjonction des clauses est représentée comme un ensemble (de clauses) puisque l’ordre des clauses n’importe pas et que chaque clause napparaît qu’une fois dans une cnf. La cnf (a ∨ b) ∧ (ā ∨ b̄) s’écrit : {ab, āb̄} La clause ab correspond à l’expression Booléenne a ∨ b. Elle n’est fausse que si a et b sont fausses. La clause ab interdit ainsi une seule valuation des variables a et b, la valuation (a = 0 et b = 0). Les trois autres valuations rendent la clause ab vraie. On remarque ainsi que toute clause interdit une seule valuation de ses variables, celle où chaque variable vaut 0 si elle apparaît positive (comme a) et 1 si elle apparaît négative (comme ā). Un théorème de l’algèbre Booléenne affirme que toute expression Booléenne peut s’écrire comme une cnf. On a donc avec les cnfs un formalisme pour exprimer n’importe quel problème combinatoire du type de ceux des figures 1 et 2. Pour ce cours, la seule maîtrise du formalisme des cnfs pour spécifier des contraintes suffit. Si vous voulez en apprendre plus sur l’algèbre de Boole, vous pouvez consulter un des nombreux ouvrages de base sur le sujet. Exercice 4. Ces exercices faciles vous familiariseront avec l’abréviation ensembliste du fomalisme des cnfs : 1. Soient trois variables Booléennes a b c. La contrainte 011 interdit la valuation a = 0 et b = 1 et c = 1. Sous forme cnf, elle s’écrit ab̄c̄. Exprimez en cnf les contraintes 101, 000, 010, 110, 111 sur a b c. 2. La conjonction des contraintes 011 et 100 sur a b c s’écrit {ab̄c̄, ābc} en cnf. Ecrivez la conjonction des contraintes 000, 001, 010, 100, 101, 110 et 111 sur a b c sous forme cnf. 8 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines Sous cette conjonction de contraintes, quelles valuations des variables a b c restent permises ? 3. Ecrivez la conjonction des 6 contraintes du problème de la figure 2 en format cnf. 4. Les contraintes du problème de la figure 2 interdisent à chaque paire de variables d’avoir la même valeur. Ecrivez en cnf les contraintes qui leur interdisent d’avoir des valeurs différentes. Quelles sont les solutions du problème ainsi spécifié ? Réponses : 1. ābc̄, abc, ab̄c, āb̄c, āb̄c̄ 2. {abc, abc̄, ab̄c, ābc, ābc̄, āb̄c, āb̄c̄}. La seule valuation permise des variables a b c est 011. 3. {ab, āb̄, ac, āc̄, bc, b̄c̄} 4. {ab̄, āb, ac̄, āc, bc̄, b̄c}. Les solutions de ce problème, c’est à dire les valuations des variables a b c qui rendent la cnf vraie, sont 000 et 111. 2.5 Le problème des n reines Il s’agit de placer n reines sur un échiquier de n sur n cases, sans qu’une reine puisse en prendre une autre. Si n = 3, il n’y a pas de solution. Cherchez à la main les solutions pour n = 4. a b c d 1 2 3 4 Fig. 4 – Le problème des 4 reines. Placer 4 reines sur l’échiquier sans qu’elles puissent se prendre directement. 2.5.1 Résolution algorithmique Exercice 5. Programmez en C le problème spécifique des 3 reines en utilisant la fonction explorer2 de la figure 3. Déclarez le tableau global tab de 3 ∗ 3 = 9 cases de type int (la 9 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines variable n de la fonction explorer2 vaut 9). Remplacez l’appel de printtab par l’appel d’une fonction VerifierContraintes. Cette fonction est déclenchée pour chacune des 23∗3 façons de placer des reines sur l’échiquier. Quand elle est déclenchée, le tableau tab contient des 0 dans les cases vides et des 1 dans les cases contenant une reine. La fonction VerifierContraintes doit vérifier que le tableau tab contient trois 1 et qu’aucune ligne, colonne ou diagonale ne contient plus d’un 1. Dans votre fonction main, vous devez appeler : explorer2(0). Quand votre fonction VerifierContraintes trouve qu’aucune contrainte n’est violée, faites-la afficher le contenu du tableau tab. Si le code est correct, l’appel explorer2(0) ne doit rien afficher, car le problème des 3 reines n’a pas de solution. La taille de l’espace de recherche exploré par la fonction explorer2 pour le problème des n reines est de 2n∗n , les 2n∗n façons de placer des reines sans contrainte sur un échiquier de n × n cases. C’est considérable. Pour réduire cet espace en préservant la complétude, il faut prouver que toutes les solutions au problème se trouvent encore dans le nouvel espace réduit. Dans le problème des n reines, on remarque que dans toute solution, chaque ligne contient une et une seule reine, et pareil pour les colonnes. En effet, si une ligne ou une colonne est vide, alors une autre ligne ou colonne devra contenir au moins 2 reines, ce qui n’est pas admis. On peut donc encoder le problème des n reines en remplaçant l’échiquier par un tableau de n entiers. Chaque case du tableau correspond à une colonne de l’échiquier et contient le numéro de la ligne où apparaît la reine de cette colonne. On démontre que chaque solution au problème des n reines appartient aux nn états possibles de ce tableau. nn est très inférieur à 2n∗n quelque soit n supérieur à 0 car 2n∗n = (2n )n . On remarque aussi que les valeurs du tableau doivent toutes être différentes car il ne peut y avoir deux reines sur une même ligne. On démontre alors que toute solution au problème des n reines s’encode avec une des permutations de la séquence 1 . . . n rangée dans le tableau. L’espace de recherche a alors une taille de n! (le nombre de permutations d’une séquence de n éléments), ce qui est évidemment très inférieur à nn , car n! = n ∗ (n − 1) ∗ (n − 2) ∗ . . . ∗ 1 et nn est le produit de même longueur : n ∗ n ∗ n ∗ . . . ∗ n. On obtient une solution beaucoup plus efficace au problème des n reines que celle qui utilise la fonction explorer2. On remplace explorer2 par une fonction qui explore les permutations d’une séquence et on adapte la fonction VerifierContraintes. Elle n’a plus qu’a vérifier les contraintes des diagonales sur le tableau d’entiers. La fonction VerifierContraintes est déclenchée n! fois, sur chaque permutation de la séquence 1 . . . n. On peut encore améliorer la recherche avec un algorithme déterministe, c’est à dire qui énumère les solutions directement, sans effectuer de recherche. Vous trouverez un tel algorithme et beaucoup d’autres informations sur le problème des n reines (aussi appelé problème des n dames) dans l’excellent article intitulé "problème des huit dames" de l’encyclopédie Wikipédia (http://fr.wikipedia.org/wiki/Huit_dames). 10 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5.2 2.5 Le problème des n reines Résolution par un raisonnement automatique général Mais pour obtenir ces améliorations, il a fallu beaucoup d’intelligence naturelle et des mathématiciens extrêmement compétents. Ce n’est pas ce que cherche l’intelligence artificielle. Une méthode de raisonnement automatique qui découvrirait et démontrerait toute seule, sans l’aide d’un mathématicien, ces améliorations successives est encore largement hors de portée. Ce qu’on possède cependant actuellement, c’est un raisonnement automatique général, qui ne connaît rien au problème des n reines ni à aucun autre problème combinatoire particulier, mais qui est particulièrement efficace pour résoudre n’importe quel problème combinatoire, une fois qu’il a été spécifié dans le formalisme général des cnfs. La spécification d’un problème en cnf reste cependant une activité complexe qui demande de l’intelligence naturelle. Ce raisonnement général se résume à une procédure, qu’on appelle un SAT-solver, qui effectue un test de satisfiabilité sur l’encodage en cnf d’un problème. Si le problème n’a pas de solution, le test de satisfiabilité échoue et le SAT-solver a ainsi déterminé que la cnf est insatisfiable. S’il a des solutions, le SAT-solver en choisit une seule au hasard le plus rapidement possible. Il suffit d’ajouter à la cnf la négation de cette solution, comme une nouvelle contrainte, puis de relancer le SAT-solver pour avoir la prochaine solution. On peut ainsi énumérer toutes les solutions d’un problème. Curieusement, la procédure qui effectue le test de satisfiabilité dans les SAT-solvers actuels est basée sur l’algorithme de Davis-Putnam (DP) qui date de 1960, lui-même basé sur l’exploration de notre procédure explorer2. La procédure explorer2 explore la table de vérité de l’expression à tester. L’algorithme DP améliore cette exploration en traitant efficacement les clauses unitaires (un seul littéral) et en simplifiant les cnfs partiellement valuées. Depuis le début des années 1990, les SAT-solvers ont considérablement amélioré l’algorithme DP sans jamais pourtant s’affranchir du coeur de la méthode, qui énumère l’espace exponentiel des valuations Booléennes, et donc risque toujours l’explosion combinatoire, ce fameux mur que vous avez expérimenté dans l’exercice 2. Mais il faut à présent des théoriciens spécialisés pour découvrir des encodages de problèmes qui piègent les actuels SAT-solvers. L’algorithme DP est actuellement désigné par le sigle DPLL, pour une version améliorée dûe à Davis Putnam Lodgeman et Loveland. Il existe une conférence annuelle internationale où s’affrontent les SAT-solvers, l’ "International Conference on Theory and Applications of Satisfiability Testing", appelée aussi SAT. Vous trouverez plein d’informations sur le site http://www.satlive.org/. 2.5.3 Encodage en cnf du problème des 3 reines Un encodage simple en cnf du problème des n reines, c’est-à-dire qui requiert peu d’intelligence naturelle, en tous cas moins que les encodages spécialisés du paragraphe 2.5.1, considère chaque case de l’échiquier comme une variable Booléenne qui vaut 0 si la 11 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines case qu’elle représente est vide et 1 si elle contient une reine. Le problème des 3 reines s’encode ainsi avec 9 variables booléennes et les contraintes qui empêchent qu’une ligne, une colonne ou une diagonale contienne plus qu’une reine, et qui obligent que l’échiquier contienne 3 reines. C’est comme un retour à l’encodage de l’exercice 5 mais le SAT-solver qui traitera la cnf obtenue aura une efficacité compétitive avec les encodages spécialisés, bien qu’il n’ait aucune connaissance spécifique du problème des n reines. Si on nomme a b c d e f g h i les 9 variables Booléennes, comme dans le tableau : a d g b e h c f i la contrainte "il y a au moins une reine dans la première ligne" s’écrit {abc}, c’est à dire a∨b∨c, qui n’est fausse que quand la ligne a b c est vide. Il suffit de coder la même contrainte pour les autres lignes. Comme il y a 3 lignes, ceci garantit que l’échiquier contient au moins 3 reines. La contrainte "il y a au plus une reine dans la première ligne" peut se traduire par une expression Booléenne qui dit que deux variables quelconques de la ligne ne peuvent être vraies en même temps. Les variables a et b ne doivent pas être vraies en même temps. C’est la contrainte Booléenne ¬(a ∧ b). Les lois de Demorgan en font la clause ā ∨ b̄, abréviée en {āb̄}. Comme il y a trois cases dans la ligne, il y a trois paires de deux variables à traiter ainsi. La contrainte complète pour la ligne a b c s’écrit : {āb̄, āc̄, b̄c̄}. Il faut coder la même contrainte "il y a au plus une reine ..." pour les autres lignes, les colonnes, les diagonales b f , a e i, d h, puis b d, c e g, f h. Exercice 6. Ecrire la cnf complète qui spécifie le problème des 3 reines. Combien contientelle de variables Booléennes ? combien de clauses ? Exercice 7. Dans le problème des n tours, il faut placer n tours sur un échiquier de n × n cases sans qu’elles se prennent. Ici, on n’a pas à encoder les contraintes de diagonales. Ecrire la cnf qui spécifie le problème des 2 tours. Combien contient-elle de variables Booléennes ? combien de clauses ? 2.5.4 Génération automatique de l’encodage La généralisation au problème des n reines pour n quelconque s’annonce délicate. Pour exprimer la contrainte "il y a au plus une reine dans la première ligne" , il faut construire un littéral par paire de cases de la première ligne. S’il y a n cases, il y a (n2 −n)/2 paires de cases et donc autant de littéraux. Ca fait une longue clause, fastidieuse à écrire à la main. Il est donc préférable d’écrire un programme qui générera ces clauses pour chaque ligne, chaque colonne et chaque diagonale. Voici une boucle qui affiche les indices de chaque paire de cases d’un tableau de n cases : 12 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines int i, j; for (i = 0; i < n-1; i++) for (j = i+1; j < n; j++) printf("%d %d\n", i, j); Si on convient d’afficher "-a" pour une variable niée ā, la boucle suivante : int i, j; for (i = 0; i < n-1; i++) for (j = i+1; j < n; j++) printf("-%d -%d\n", i, j); affiche les (n2 − n)/2 clauses qui imposent la contrainte "il y a au plus ..." sur les variables Booléennes 1 . . . n. Exercice 8. Ecrire une fonction C qui affiche la cnf du problème des n tours (exercice 7). Les contraintes sont : pas plus d’une tour par ligne ou par colonne, n tours exactement sur l’échiquier n × n. Pour les contraintes des diagonales du problème des reines, une façon simple de procéder est de recopier les noms des variables Booléennes de chaque diagonale dans un tableau d’entiers et traiter le tableau de la même façon, qu’il s’agisse d’une ligne, d’une colonne ou d’une diagonale. Pour parcourir les diagonales, et même pour les lignes et les colonnes, il est pratique de faire correspondre les noms des cases comme coordonnées cartésiennes sur l’échiquier, et comme variables Booléennes numérotées 1 . . . n ∗ n, par exemple comme dans le tableau de la figure 5. 1 2 3 1 1 2 3 2 4 5 6 3 7 8 9 Fig. 5 – Correspondance entre les coordonnées cartésiennes des cases et les noms des variables Booléennes de l’encodage en cnf. La macro VAR(ligne, colonne, taille) ci-dessous effectue cette correspondance. Pour chaque paire de coordonnées (ligne, colonne), VAR retourne le numéro de variable Booléenne correspondant pour un échiquier de taille x taille cases : 13 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines #define VAR(ligne, colonne, taille) (taille*((ligne)-1)+(colonne)) Dans l’autre sens, à partir du numéro i d’une variable, on récupère les coordonnées (ligne, colonne) correspondantes avec ((i − 1)/taille) + 1 et ((i − 1)%taille) + 1. Les boucles suivantes parcourent toutes les diagonales d’un échiquier n × n : int ligne, colonne, i, j, k; // diagonales sens 1 for (ligne = 1; ligne < n; ligne++) for (i = ligne, k = 1; i <= n printf("%d ", VAR(i, k, printf("\n"); } for (ligne = 2; ligne < n; ligne++) for (i = ligne, k = 1; i <= n printf("%d ", VAR(k, i, printf("\n"); } { ; i++, k++) n)); { ; i++, k++) n)); // diagonales sens 2 for (colonne = n; colonne > 1; colonne--) { for (i = colonne, k = 1; i > 0; i--, k++) printf("%d ", VAR(i, k, n)); printf("\n"); } for (ligne = 2; ligne < n; ligne++) { for (i = n, k = ligne, j = 0; k <= n; i--, k++, j++) printf("%d ", VAR(i, k, n)); printf("\n"); } Exercice 9. Compilez ce code et exécutez-le pour n = 3. Vérifiez sur la figure 5. Projet 1. Avec la numérotation des variables Booléennes comme sur la figure 5, généralisée à l’échiquier n*n, écrivez un programme C complet qui imprime toutes les clauses de la cnf qui encode les contraintes du problème des n reines : – Au moins une reine par ligne – Au plus une reine par ligne – Au moins une reine par colonne – Au plus une reine par colonne 14 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines – Au plus une reine par diagonale Combien y-a-t-il de variables et combien de clauses (en fonction de n) ? 2.5.5 Résolution avec un SAT-solver Il existe beaucoup de SAT-solvers librement disponibles sur Internet : zchaff, minisat2, eqsatz, HaifaSat, RSat, tinisat, Jerusat, lsat, minimarch, PracticalSAT, satz ... Cherchez aussi "SAT solver" sur Wikipedia. Nous allons utiliser zchaff. Ce n’est pas le plus efficace mais c’est probablement le plus simple à mettre en oeuvre et son efficacité est déjà redoutable. Il a été vainqueur de la compétition SAT pendant plusieurs années. Il est open source et téléchargeable sur le site de l’université de Princeton. Vous pouvez soit chercher "zchaff" sur Google, soit directement vous rendre à l’url http://www.princeton.edu/ ~chaff/zchaff.html. Là, choisissez la version 32 bits, ou 64 bits si vous avez une machine 64 bits. Il vous faudra formuler la raison pour laquelle vous voulez utiliser zchaff. Dites par exemple que c’est pour le cours d’intelligence artificielle de l’i.e.d. Si vous voulez un jour inclure zchaff dans un produit que vous voudrez commercialiser, vous devrez négocier avec les auteurs de zchaff. Vous recevrez une archive zChaff2007.3.12.tar (si elle est compressée, décompressez-la avec gunzip). Sous Linux ou Unix ou sur MAC OS X, vous désarchivez avec la commande tar -xvf zChaff2007.3.12.tar Le dossier obtenu contient le code source de zchaff. Compilez avec la commande make zchaff Si tout se passe bien, vous devez obtenir un fichier exécutable nommé zchaff. Si vous êtes sous Windows, vous devrez construire vous-même votre projet dans l’environnement de programmation en C++ qui vous est familier, et y intégrer le source des fichiers .cpp. Le code est relativement portable et s’il y a des modifications à apporter, elles sont mineures. Toujours sous Windows, vous pouvez essayer le SAT solver WinSat, fait pour Windows (http://users.ecs.soton.ac.uk/mqq06r/winsat/) mais vous n’aurez pas le source et vous ne pourrez pas l’utiliser comme une librairie. Si vous êtes bloqué, vous pouvez essayer aussi minisat2 (même procédure, cherchez sur google, téléchargez, intallez) ou consulter la page "SAT solver" de Wikipédia. Le fichier SAT.h contient une documentation pour utiliser zchaff comme une librairie dans un programme C ou C++ de votre conception. Pour le moment, nous allons utiliser zchaff comme une simple commande, qui prend en entrée un fichier qui contient la cnf spécifiant un problème, et qui imprime en sortie (le terminal où vous avez tapé la commande zchaff) soit le message "unsatisfiable", soit une première solution au problème spécifié. 15 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines Le format DIMACS On conviendra de donner l’extension .cnf aux fichiers qui contiennent les cnfs à soumettre à zchaff. Pour les cnfs, zchaff s’attend à un format d’entrée international, le "DIMACS CNF format", décrit dans un document .ps ou .pdf téléchargeable sur Internet (par exemple ici : http://www.cs.ubc.ca/~hoos/SATLIB/Benchmarks/SAT/ satformat.ps). Pour représenter la cnf {ab̄, bc̄} dans un fichier f.cnf en format DIMACS, il faut d’abord renommer les variables a b c en 1 2 3. On obtient la cnf {12̄, 23̄}. Le fichier f.cnf à fournir à zchaff contiendra : c p 1 2 Un exemple de fichier en format DIMACS cnf 3 2 -2 0 -3 0 Les lignes commençant par le caractère "c" sont des commentaires, ignorés par zchaff. La ligne commençant par le caractère "p" est obligatoire. Elle a le format : p format nombre-de-variables nombre-de-clauses Ici, le format est "cnf". Le reste des lignes décrit les clauses de la cnf. Chaque clause est terminée par le caractère 0. Les variables Booléennes sont toujours numérotées de 1 à n, sans interruption dans la séquence. Les variables niées, comme 2̄, sont précédées du signe "-", comme -2. Utilisation du SAT solver Une fois que le fichier f.cnf est créé, il suffit de lancer la commande suivante : zchaff f.cnf Il s’imprime ceci : Z-Chaff Version: zChaff 2007.3.12 Solving f.cnf ...... c 2 Clauses are true, Verify Solution successful. Instance Satisfiable 1 -2 -3 Random Seed Used 0 Max Decision Level 1 Num. of Decisions 2 ( Stack + Vsids + Shrinking Decisions ) 0 + 1 + 0 Original Num Variables 3 Original Num Clauses 2 Original Num Literals 4 16 c 2008 D. Goossens Université Paris 8 - [email protected] 2.5 Le problème des n reines Added Conflict Clauses 0 Num of Shrinkings 0 Deleted Conflict Clauses 0 Deleted Clauses 0 Added Conflict Literals 0 Deleted (Total) Literals 0 Number of Implication 3 Total Run Time 7.4e-05 RESULT: SAT Le résultat SAT signifie que la cnf est satisfiable. zchaff imprime une solution à la ligne : 1 -2 -3 Random Seed Used 0 Cette solution est la valuation 1 = vrai, 2 = f aux, 3 = f aux, qui effectivement rend la cnf vraie, c’est à dire vérifie toutes ses contraintes. Exercice 10. Construisez un fichier diffs.cnf et représentez-y la cnf {ab, āb̄, ac, āc̄, bc, b̄c̄} en format DIMACS. Vous devrez numéroter les variables a b c en 1 2 3. N’oubliez pas la ligne qui commence par le caractère "p". Soumettez ce fichier à zchaff. Le résultat doit être UNSAT. Exercice 11. Ecrivez un programme qui crée un fichier reines4.cnf et y imprime la spécification cnf en format DIMACS du problème des 4 reines. Dans la ligne "p cnf nbvariables nbclauses", pour zchaff, le nombre de clauses peut être supérieur au nombre réel, donc vous pouvez mettre un grand nombre quelconque, comme 10000. Voici un exemple de code de démarrage : #include <stdio.h> FILE *f; int nbvars = 16; int nbclauses = 10000; int main () { f = fopen("reines4.cnf", "w"); if (f == NULL) return 1; fprintf(f, "p cnf %d %d\n", nbvars, nbclauses); reines(4); fclose(f); return 0; } 17 c 2008 D. Goossens Université Paris 8 - [email protected] 2.6 Un formalisme intermédiaire : les CSP Il vous reste à écrire la fonction reine, qui imprime les clauses. Compilez, exécutez puis soumettez le fichier reines4.cnf à zchaff. Dans la solution imprimée, repérez les variables positives (sans "-" devant). Ce sont celles qui sont vraies, et qui représentent donc les cases qui contiennent une reine. Pour interdire uniquement cette solution, il suffit de construire une clause avec ces variables niées. Ajoutez cette clause au fichier et re-soumettez à zchaff. Recommencez tant qu’il y a des solutions. Combien y-a-t-il de solutions au problème des 4 reines ? Projet 2. L’objectif de ce projet est de comparer l’efficacité de zchaff avec celle de solutions algorithmiques évoquées au paragraphe 2.5.1. Ecrivez un programme qui généralise l’exercice 11 au problème des n reines. Générez les fichiers cnf pour n = 50, 60, 70, 80, 90, 100, ... et recopiez les temps de calcul affichés par zchaff dans un fichier .txt que vous fournirez à gnuplot ou excel ou tout autre tableur. Déterminez la première valeur de n pour laquelle le temps de calcul dépasse la minute (remarque : des valeurs plus grandes encore de n peuvent mettre moins de temps. C’est normal). Faites de même pour une solution algorithmique de votre choix, que vous fabriquez vousmême ou que vous récupérez sur Internet (cherchez "problème des n reines" sur Google). En principe, zchaff devrait être moins efficace qu’une solution spécialisée qui ne fonctionne que pour le problème des n reines. Mais en réalité, zchaff devrait se montrer compétitif avec la plupart. 2.6 Un formalisme intermédiaire : les CSP Le problème des n reines requiert entre autres deux contraintes sur un ensemble de variables Booléennes : celle qui exige qu’une au moins des variables soit vraie, et celle qui exige qu’une au plus soit vraie. Ce motif apparaît systématiquement, dans la grande majorité des problèmes. Les deux contraintes ensemble exigent qu’une variable Booléenne exactement, parmi un ensemble de variables Booléennes, soit vraie, et que donc toutes les autres soient fausses. On a vu qu’on pouvait spécifier le problème des n reines avec un ensemble de n variables non Booléennes. Dans la description d’une solution, chacune de ces variables correspond à une colonne de l’échiquier et contient le numéro de la ligne où apparaît la reine (voir l’exemple de la figure 6). Si la variable a représente la première colonne, elle contient un seul nombre parmi 1 . . . n. On dit que l’ensemble {1 . . . n} est le domaine de la variable a. On dit que a est une variable à domaine fini, et on note a ∈ {1 . . . n}. En algèbre de Boole, cette situation peut être représentée par n variables Booléennes. Chacune de ces variables correspond à une des propositions a = 1, . . . , a = n. On impose la contrainte qu’une seule de ces variables est vraie. Exercice 12. Ecrivez deux fonctions auMoins1 et auPlus1. La fonction auMoins1(n) imprime la clause 1 ∨ . . . ∨ n en format DIMACS. La fonction auPlus1(n) imprime, pour 18 c 2008 D. Goossens Université Paris 8 - [email protected] a b 2.6 Un formalisme intermédiaire : les CSP c 1 a b c 1 3 2 2 3 Fig. 6 – Le problème des 3 reines encodé avec 3 variables à domaine fini. Les trois variables a b c du tableau de droite encodent l’état de l’échiquier de gauche (qui n’est pas une bonne solution). chaque paire de variables i, j, la clause ī ∨ j̄ en format DIMACS, en utilisant la double boucle vue au paragraphe 2.5.4. Les deux fonctions auMoins1 et auPlus1 imposent la contrainte "une seule exactement des variables est vraie" sur un ensemble de variables {1 . . . n}. Si cet ensemble représente le domaine d’une variable à domaine fini x, on a ainsi enrichi notre formalisme de spécification, jusque là réduit aux cnfs, avec deux nouvelles constructions qui le rendent plus pratique. On peut spécifier le problème des 3 reines avec 3 variables a b c à domaine fini, correspondant aux trois colonnes de l’échiquier (voir figure 6) : Variables et domaines a ∈ {1, 2, 3}, b ∈ {1, 2, 3}, c ∈ {1, 2, 3} Contraintes ab ∈ {13, 31}, bc ∈ {13, 31}, ac ∈ {12, 21, 23, 32} Cette spécification est un problème de satisfaction de contraintes (Constraint Satisfaction Problem, ou CSP). Pour la partie Variables et domaines, nos fonctions auMoins1 et auPlus1 fournissent la traduction en cnf. La partie Contraintes énumère toutes les paires de variables. Pour chaque paire, on énumère les valeurs permises. Pour la paire des deux variables a et b, par exemple, seules les valuations a = 1 ∧ b = 3 et a = 3 ∧ b = 1 ne violent pas les contraintes de diagonales. Dans la solution de la figure 6, on remarque que la valuation b = 3 ∧ c = 2 n’appartient pas au domaine de la paire bc et ne peut donc pas être retenue. D’une manière générale, la partie Contraintes d’un CSP associe à des n-uplets (paires, triplets ...) de variables choisis un sous-ensemble du produit cartésien de leurs domaines. Ce sous-ensemble est soit l’ensemble des valeurs permises pour le n-uplet, soit des valeurs interdites. Les valeurs interdites permettent la traduction en cnf la plus directe. Traduisons la contrainte ab ∈ {13, 31}. Ce sont les valeurs permises. Le complémentaire dans le produit cartésien contient les valeurs interdites : ab 6∈ {11, 12, 21, 22, 23, 32, 33}. Il suffit de 19 c 2008 D. Goossens Université Paris 8 - [email protected] 2.7 Les cryptogrammes construire les 7 clauses correspondantes en format DIMACS. On commence par la traduction évidente en l’expression Booléenne : ¬(a = 1 ∧ b = 1) ∧ ¬(a = 1 ∧ b = 2) ∧ ¬(a = 2 ∧ b = 1) ∧ ¬(a = 2 ∧ b = 2) ∧ ¬(a = 2 ∧ b = 3) ∧ ¬(a = 3 ∧ b = 2) ∧ ¬(a = 3 ∧ b = 3). On traduit en cnf avec la réduction de Demorgan : (¬(a = 1) ∨ ¬(b = 1)) ∧ (¬(a = 1) ∨ ¬(b = 2)) ∧ (¬(a = 2) ∨ ¬(b = 1)) ∧ (¬(a = 2) ∨ ¬(b = 2)) ∧ (¬(a = 2) ∨ ¬(b = 3)) ∧ (¬(a = 3) ∨ ¬(b = 2)) ∧ (¬(a = 3) ∨ ¬(b = 3)). Ensuite, on numérote les variables Booléennes qui représenteront les propositions a = 1, a = 2 . . . pour le format DIMACS. Si on utilise la numérotation de la figure 5, la proposition a = 1 correspond à la variable 1, a = 2 à la variable 2, etc. On obtient la cnf {1̄2̄, 1̄5̄, 4̄2̄, 4̄5̄, 4̄8̄, 7̄5̄, 7̄8̄}. La clause 1̄2̄, par exemple, s’imprime en format DIMACS : -1 -2 0. Exercice 13. Traduisez de la même façon la contrainte ac ∈ {12, 21, 23, 32}. Mettons tout ça en oeuvre sur un problème différent. 2.7 Les cryptogrammes Les cryptogrammes sont des messages encryptés qu’il faut décrypter. Voici un cryptogramme classique. C’est une addition encryptée. Il faut remplacer les lettres par des chiffres (de 0 à 9) en respectant des contraintes. Différentes occurrences d’une même lettre doivent être remplacées par un même chiffre. Des lettres différentes doivent être remplacées par des chiffres différents. L’addition finale doit être juste. + D G R O E O N R B A A E L L R D D T Il n’est pas facile de traduire directement un tel problème en une cnf, ni même en une expression Booléenne. Par contre, la spécification en CSP est plus intuitive. On choisit 10 variables, correspondant aux 10 lettres de l’addition. On associe à chaque variable le domaine unique {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. La traduction en cnf de ces variables à domaine fini prend en compte le fait que chaque lettre doit être remplacée par un chiffre exactement, et que différentes occurrences d’une même lettre doivent être remplacées par un même chiffre. Il reste à coder en cnf le fait que des lettres différentes doivent être remplacées par des chiffres différents. Il suffit d’utiliser une paire de contraintes auMoins1 et auPlus1 sur les variables Booléennes qui encodent tous les états possibles d’une même lettre. Ca revient à considérer chaque chiffre comme une variable dont le domaine est l’ensemble des 10 lettres. Ni les CSP ni les cnfs n’offrent de primitive pour encoder les connaissance arithmétiques. Les SAT solvers sont des programmes de raisonnement généraux. Ils n’ont aucune connaissance en arithmétique. Pour encoder la contrainte "l’addition finale doit être juste", 20 c 2008 D. Goossens Université Paris 8 - [email protected] 2.7 Les cryptogrammes il va falloir encoder en expression Booléenne la table d’addition, en lien avec les lettres du problème. Prenons la dernière colonne de l’addition encryptée : D + D = T . C’est une contrainte sur les variables D et T . Elle interdit par exemple la valuation D = 2 ∧ T = 5. En fait, chaque valeur choisie pour D détermine celle de T . En algèbre de Boole, ceci incite à utiliser l’implication ⇒. On a par exemple (D = 1) ⇒ (T = 2). En algèbre de Boole, x ⇒ y est équivalent à x̄ ∨ y. Il y a ici deux sortes de difficultés à surmonter. D’abord la génération par programme des tables d’addition usuelles, de 0+0=0 jusqu’à 9+9=18 et ensuite la traduction des colonnes de l’addition en expressions Booléennes contenant ces implications puis la traduction de ces expressions en format cnf. Notons que la conjonction D = 0∧T = 0, dûe à 0+0=0 de la table d’addition, viole la contrainte "des chiffres différents pour des lettres différentes" mais son ajout à l’encodage final ne pose pas de problème car cette dernière contrainte fait partie de l’encodage final et ne retiendra aucune solution où D = 0 et T = 0 sont vrais ensemble. Une autre difficulté technique de ce problème est le mécanisme des retenues. Si D = 6, par exemple, T = 2, car 6+6=12. On retient 1, et la retenue est ajoutée à la colonne précédente : 1 + L + L = R. Pour modéliser ce mécanisme, il faut ajouter 5 nouvelles variables, correspondant aux retenues des 5 premières colonnes de l’addition. Dans une addition, une retenue est 0 ou 1. On peut donc utiliser 5 variables Booléennes. Dans le format DIMACS final, il faudra numéroter les 10*10=100 variables du type lettre=chiffre et les 5 variables Booléennes représentant les retenues. On aura 105 variables, numérotées de 1 à 105. Projet 3. Ecrivez un programme (dans le langage de votre choix) qui construit un fichier donald.cnf contenant la spécification en format DIMACS du cryptogramme DONALD+GERALD=ROBERT. Utilisez un SAT solver (par exemple zchaff ) pour en trouver toutes les solutions. 21 c 2008 D. Goossens