Consignes

Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant un répertoire TP1-NOM avec :

Vous aurez compris que NOM devra être remplacé par votre nom (ou vos noms en cas de binomes) !

Tous vos fichiers doivent comporter une entête avec votre nom et votre groupe de TP.

Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note !

Liens utiles

Préliminaires : naivesat.py, un solveur récursif naïf en Python

Cette première séance de travaux pratiques doit vous aidez à vous familiariser avec le problème de la satisfiabilité : SAT.

Étant donnée une formule (données en forme normale conjonctive), comme (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4) nous voulons savoir s'il est possible de satisfaire cette formule (càd la rendre vraie) en choisissant des valeurs booléennes pour x1, x2, x3 et x4.

Le problème de la satisfiabilité fera également l'objet du TP 2, où il faudra améliorer un solveur écrit en C.

Le format 'SAT' de D. Knuth pour les formules en forme normale conjonctive

Les formules considérées seront toujours en forme normale conjonctive. Elle seront en général données dans des fichiers de type 'SAT' (extension .sat).

Voici par exemple la représentation de la formule (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4) :

~ un exemple de formule toute simple dans un fichier SAT
x1 ~x2 x3
~x1 ~x3 x4
x2 ~x4

Utilisation de naivesat

Le fichier naivesat-NOM.py contient un solveur SAT récursif naïf écrit en Python.

Vous devrez le compléter (question 1) pour le rendre fonctionnel, mais voici comment il s'utilise pour chercher une solution

$ ./naivesat-NOM.py FICHIER_FORMULE

FICHIER_FORMULE est le fichier (au format SAT) qui contient la formule à considérer

Par exemple :

$ ./naivesat-NOM.py test2.sat
SATISFIABLE
~a b c d
$ ./naivesat-NOM.py test3.sat
UNSATISFIABLE

Dans le premier cas, la formule est satisfiable avec les valeurs a=0, b=c=d=1, alors que dans le second cas, la formule n'est pas satisfiable.

Deux autres fonctionnalités pratiques de naivesat-NOM.py sont :

Code du solveur

En interne, les formules traitées par naivesat-NOM.py sont des listes de listes d'entiers (non nuls).

La première fonction définie dans naivesat-NOM.py est la fonction solve. Cette fonction ne fait que quelques lignes et les commentaires devraient vous aidez à comprendre comment elle fonctionne.

La deuxième fonction, utilisée par la fonction solve est simplify_formula. Cette fonction prend en argument une formule (càd une liste de listes d'entiers, voir plus haut) et un littéral (càd un entier non nul). Cette fonction doit simplifier la formule lorsqu'on suppose que le littéral fourni est vrai.

Par exemple, si on suppose que le littéral x2 est vrai :

avant simplification : (x1 ¬x2 x3) (¬x1 ¬x3 x4) (x2 ¬x4)
après simplification : (x1 x3) (¬x1 ¬x3 x4)

En effet,

La fonction simplify_formula doit reconstruire une liste de clauses à partir de la liste de clauses F :

ATTENTION, il ne faut pas modifier la formule ou les clauses, mais en recréer de nouvelles. C'est pour cela qu'on crée une nouvelle formule simplified_F et qu'il faudra recopier tous les littéraux de chaque clause pour en créer de nouvelles.

Vous ne devez par exemple pas utiliser la méthode remove des listes Python !

Je vous rappelle également que la bonne manière d'insérer un élément dans une liste est d'utiliser la méthode append. N'utilisez pas L = L+[element] !

  1. Complétez le corps de la fonction simplify_formula et testez la. Vous pouvez pour cela utiliser l'option -T ou --test pour lancer la fonction de tests :

    $ ./naivesat-NOM.py test.sat --test 1
    simplification avec 1
    avant : [[1, 2, -3], [2, 3, -4], [3, 4, 1], [4, -1, 2], [-1, -2, 3], [-2, -3, 4], [-3, -4, -1], [-4, 1, -2]]
    après : [[2, 3, -4], [4, 2], [-2, 3], [-2, -3, 4], [-3, -4]]

    N'hésitez pas à modifier la fonction de tests.

  2. Testez le solveur sur la formule précédente (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4).

    Le résultat est il bien correct ?

Attention, pour donner un argument négatif, il faut ajouter "--" après --test ou -T. Sinon, le -1 est interprété comme une option de la commande !

$ ./naivesat-NOM.py test.sat --test -- -1
simplification avec 1
avant : [[1, 2, -3], [2, 3, -4], [3, 4, 1], [4, -1, 2], [-1, -2, 3], [-2, -3, 4], [-3, -4, -1], [-4, 1, -2]]
après : [[2, -3], [2, 3, -4], [3, 4], [-2, -3, 4], [-4, -2]]
  1. La formule (a ∨ ¬b) ∧ (¬b ∨ ¬c) ∧ (¬a ∨ ¬c) ∧ (¬a ∨ ¬b ∨ c) ∧ (a ∨ b ∨ ¬c) est elle satisfiable ?

    Créez un fichier question2-NOM.sat que vous joindrez à votre archive pour tester.

  2. Y'a t'il d'autres solution que celle trouvée ? Comment procédez vous pour les trouver ? (Indice: il faudra rajouter des clauses pour interdire la solution trouvée)

Pour créer un fichier .sat, vous pouvez utiliser n'importe quel éditeur de texte : SublimeText, Gedit, nano, etc.

Rechercher toutes les solutions

Pour les petites formules, il est parfois intéressant de chercher toutes les solutions à un problème.

  1. En partant du code de la fonction solve, écrivez une fonction all_solutions qui affiche toutes les solutions, au fur et à mesure qu'elle les trouve.

    Cette fonction aura exactement le même "squelette" que solve, mais :

    • elle ne renvoie pas de solution, elle les affiche avec la fonction print_sol,

    • elle explore toujours les deux alternative "+lit" et "-lit".

    Il ne sera donc plus possible d'utiliser le or du Python qui ne calcule pas la seconde alternative.

  2. Essayez (sans y passer trop de temps) de renvoyer le nombre de solutions trouvées.

Vous pouvez appeler la fonction all_solutions sur un fichier avec l'option -A ou --all de naivesat-NOM.py.

Testez plusieurs fois naivesat-NOM.py avec l'option --all sur la formule précédente) en mélangeant aléatoirement la formule (option -r / --randomize sur la ligne de commande).

Que constatez vous ? Proposez une explication.

1. Quelques problèmes de satisfiabilité

Il est possible d'encoder de nombreux problèmes comme des problèmes de satisfiabilité. Par exemple, les règles d'un sudoku s'expriment comme des contraintes logiques :

Bien sûr, il faut utiliser un programme pour générer une telle formule !

Écrivez un programme qui génère un fichier SAT à partir d'une grille de sudoku, et qui affiche la grille obtenu à partir de la solution trouvée par le solveur.

Attention, ceci se rapproche plutôt d'un mini projet que d'une simple question. Ne commencer à y réfléchir qu'une fois que vous avez fait le reste du TP !

(Pour information, mon fichier Python qui permet de faire ceci fait un peu moins de 150 lignes.)

1.1. L'énigme de Einstein

Comme naivesat-NOM.py ne peut pas (encore) gérer un tel nombre de variables / clauses (voici un exemple de fichier SAT correspondant à une grille de sudoku), nous allons regarder un autre problème "amusant" : une variante de l'énigme d'Einstein.

échauffement

Ce problème est extrait du lien suivant : http://villemin.gerard.free.fr/LogForm/Integram.htm.

On suppose que les 4 cartes suivantes sont les 4 as :

1 2 3 4
Images/card_back.png Images/card_back.png Images/card_back.png Images/card_back.png

Et on dispose des indices suivants :

  1. En 2, il n'y a ni l'as de trèfle, ni l'as de pique.

  2. L'as de trèfle est plus à droite que l'as de carreau.

  3. L'as de carreau et l'as de cœur ne sont pas à côté l'un de l'autre.

Question : quelles sont les positions des as ? La solution est elle unique ?

  1. Écrivez (à la main) un fichier SAT pour résoudre le problème.

    • il y aura 16 variables :

      • X_1_trefle pour l'as de trèfle est en position 1

      • X_2_trefle pour l'as de trèfle est en position 2

      • ...

      • X_3_coeur pour l'as de coeur est en position 3

      • ...

    • le premier indice donne 2 clauses ne contenant qu'un littéral

    • le deuxième indice donne 4 clauses qu'il vous faudra transformer en FNC (c'est facile) :

      • X_4_trefle ⟶ X_1_carreau ∨ X_2_carreau ∨ X_3_carreau

      • X_3_trefle ⟶ X_1_carreau ∨ X_2_carreau

      • X_2_trefle ⟶ X_1_carreau

      • ¬X_1_trefle (en effet, l'as de trèfle ne peut pas se trouver en position 1)

    • le troisième indice donne 6 clauses de 2 littéraux chacune. Une de ces clauses est ¬(X_2_carreau ∧ X_3_coeur). (Là encore, la transformation en FNC est très facile.)

    Il y a souvent plusieurs choix possibles pour exprimer les contraintes. Formellement, l'indice 1 s'exprime par les 2 clauses (~X_2_trefle) ∧ (~X_2_pique), mais la clause (X_2_coeur ∨ X_2_carreau) pourrait aussi être utilisée.

    Cette deuxième possibilité est en une conséquence de l'indice 1 et de la contrainte logique (X_2_pique ∨ X_2_coeur ∨ X_2_carreau ∨ X_2_carreau). Autrement dit, interpréter l'indice par cette clause nécessite une étape de raisonnement de la part de l'utilisateur, alors que la première solution est directe.

    Il vous faudra aussi ajouter d'autres contraintes. La plus complexe sera pour dire, par exemple, qu'il n'y a qu'un seul as de pique. Les clauses correspondantes, au format SAT, sont :

    ~  il y a au plus un as de pique
    ~X_1_pique ~X_2_pique
    ~X_1_pique ~X_3_pique
    ~X_1_pique ~X_4_pique
    ~X_2_pique ~X_3_pique
    ~X_2_pique ~X_4_pique
    ~X_3_pique ~X_4_pique

    Avec une utilisation judicieuse du copier/coller, l'écriture de ce fichier ne devrait pas vous prendre trop de temps. Je vous conseille également de mettre des commentaires pour expliquer à quoi correspond chaque groupe de clauses.

  2. Cherchez une solution au problème avec naivesat-NOM.py, et vérifiez qu'elle est bien cohérente avec les indices.

    Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.

Le problème de Einstein

Voici maintenant une variante de l'énigme d'Einstein.

On suppose que 5 maisons sont alignée, en positions 1, 2, 3, 4, 5:

1 2 3 4 5
./Images/small_house-1.png ./Images/small_house-2.png ./Images/small_house-3.png ./Images/small_house-4.png ./Images/small_house-5.png

Chaque maison est habitée par une personne :

  • qui a un animal

  • qui boit une boisson particulière

  • qui écoute un style de musique

  • qui exerce un métier

On sait que

  1. Le musicien a une tortue.

  2. Le propriétaire de la maison jaune écoute du jazz.

  3. La personne qui écoute du hard-rock a un chat.

  4. La maison en bois se trouve juste à gauche de la maison blanche.

  5. Le chercheur habite dans la maison en briques.

  6. Le footballeur habite juste à coté de l'immeuble.

  7. La personne qui écoute de la musique classique est voisine de celle qui boit de la bière.

  8. La personne qui écoute du hiphop boit du thé.

  9. La personne qui habite dans la maison du milieu boit beaucoup de soda.

  10. Le footballeur habite dans la première maison.

  11. La personne qui écoute de la musique classique est voisine de la personne qui a des poissons.

  12. Le boulanger écoute de la chanson française.

  13. La personne qui a un hamster est voisine de celle qui écoute du jazz.

  14. L'architecte boit de l'eau.

  15. Le propriétaire de la maison en bois aime bien le whisky.

Question : qui est le propriétaire du zèbre ?

L'expression de ce problème comme problème de satisfiabilité utilisera 125 variables ! Il est donc préférable de générer la formule par un programme.

Les variables

Les variables seront toutes de la forme E_i_a pour dire "l'attribut a est à l'Emplacement i"

Les attributs sont les types de maisons (5 possibilités), animaux (5 possibilités), boissons (5 possibilités), styles de musique (5 possibilité), métiers (5 possibilités). Il y a donc 25 attributs et 5 emplacements possibles. Cela fait bien un total de 25 × 5 = 125 variables.

Par exemple, la formule E_3_musicien ∧ E_3_brique indique que le musicien habite dans la maison en briques, et que cette maison est en position 3.

Les indices

La plupart des indices sont de la forme "les attributs a et b sont au même endroit". Par exemple, le premier indice est équivalent à "le musicien et la tortue sont au même endroit".

On peut exprimer ceci par la formule

(E_1_musicien ⟶ E_1_tortue)
(E_2_musicien ⟶ E_2_tortue)
(E_3_musicien ⟶ E_3_tortue)
(E_4_musicien ⟶ E_4_tortue)
(E_5_musicien ⟶ E_5_tortue)

qu'il vous faudra transformer en FNC (5 clauses de 2 littéraux).

Nous allons utiliser 4 types d'indices :

Les fonctions left_of, next_to et exact sont déjà définies dans le fichier zebra-NOM.py.

Les contraintes

Il faut à ceci ajouter 3 autres contraintes logiques :

Les fonctions constraints_2 et constraints_3 sont déjà définies dans le fichier zebra-NOM.py.

  1. Récupérez le fichier zebra-NOM.py et

    • écrivez le corps de la fonction implies,

    • écrivez le corps de la fonction constraints_1,

    • complétez avec les appels pertinents pour chacun des indices.

    Ce programme, lorsqu'on le lance avec

    $ ./zebra-NOM.py
    ...

    affiche à l'écran la formule CNF correspondant au problème de Einstein.

  2. lancez naivesat-NOM.py sur la formule obtenue.

    Vous pouvez sauvegarder la formule générée par le programme zebra-NOM.py dans un fichier avec

    $ ./zebra-NOM.py > formule-zebre.sat

    et utiliser naivesat-NOM.py comme précédemment, "brancher" les 2 programmes zebra-NOM.py et naivesat-NOM.py avec

    $ ./zebra-NOM.py | ./naivesat-NOM.py
  3. Répondez à la question "Qui est le propriétaire du zèbre ?".

L'ordre des clauses d'une formule est parfois important.

Essayer de résoudre le problème en mélangeant la formule (option -r / --random), ou simplement en mettant les contraintes logiques (appels aux fonctions constraints_1, constraints_2 et constraints_3) avant les indices pour constater une grosse différence de temps de calcul !

Normalement, il n'y a qu'une seule solution au problème ! Si vous lancez naivesat-NOM.py avec l'option -A / --all, vous devriez obtenir 2 solutions. (Le propriétaire du zèbre est le même dans les deux.)

Essayez de comparer les solutions afin de trouver le bug que j'ai introduit dans le fichier zebra-NOM.py.

1.2. Arithmétique

Étant donné un circuit booléen, il est facile de calculer la sortie à partir des entrées. Avec un solveur, on peut essayer de faire l'inverse : étant donné le résultat, trouver une entrée correspondante !

Les entiers d'un ordinateur sont simplement des suites de (32 ou 64) bits. Les opérations sont faites par des circuits, c'est à dire des opérations logiques sur ces bits. Nous allons essayer d'inverser les circuits correspondants à des opérations arithmétique.

Inverser l'addition

Par exemple, l'addition de 2 nombres écrits en binaire se fait en additionnant leurs bits avec la retenue colonne par colonne.

Pour additionner les nombres ak ... a1 a0 et bk ... b1 b0, on peut "poser l'addition"

cn .......... c2 c1 c0 (retenues)
an .......... a2 a1 a0 (1er nombre)
+ bn .......... b2 b1 b0 (2ème nombre)

sn .......... s2 s1 s0 (résultat)

Un des exercice de la première feuille de TD consistait à calculer les formes normales conjonctives des formules s ⇔ (a ⊕ b ⊕ c) et cc ⇔ (a ∧ b) ∨ (a ∧ c) ∨ (b ∧ c). On avait trouvé respectivement

(¬a ∨ ¬b ∨ ¬c ∨ s) (¬a ∨ ¬b ∨ c ∨ ¬s) (¬a ∨ b ∨ ¬c ∨ ¬s) (a ∨ ¬b ∨ ¬c ∨ ¬s)
(a ∨ b ∨ c ∨ ¬s) (a ∨ b ∨ ¬c ∨ s) (a ∨ ¬b ∨ c ∨ s) (¬a ∨ b ∨ c ∨ s)

et

(¬a ∨ ¬b ∨ ¬c ∨ cc) (¬a ∨ ¬b ∨ c ∨ cc) (¬a ∨ b ∨ ¬c ∨ cc) (a ∨ ¬b ∨ ¬c ∨ cc)
(a ∨ b ∨ c ∨ ¬cc) (a ∨ b ∨ ¬c ∨ ¬cc) (a ∨ ¬b ∨ c ∨ ¬cc) (¬a ∨ b ∨ c ∨ ¬cc)

Créez un fichier xor3-NOM.sat approprié, et en utilisant votre fonction all_solutions, vérifiez que les solutions sont effectivement les valeurs de a, b, c et s qui vérifient la formule s ⇔ (a ⊕ b ⊕ c).

On a s = (a ⊕ b ⊕ c) ssi 0 = a ⊕ b ⊕ c ⊕ s, c'est à dire ssi un nombre pair de variables sont à 1. Il suffit donc de vérifier à la main ceci sur les solutions, et de vérifier qu'on a bien toutes les possibilités.

Téléchargez le fichier addition-NOM.py.

Écrivez une fonction xor3(s, a, b, c) qui affiche les clauses correspondant à l'opération (sur des bits) s := a ⊕ b ⊕ c.

Inspirez-vous pour cela de la fonction carry3(cc, a, b, c) qui affiche les clauses correspondant à l'opération cc := (a ∧ b) ∨ (a ∧ c) ∨ (b ∧ c).

  1. Complétez le fichier addition-NOM.py pour générer un problème SAT correspondant au circuit addition de résultat s :

    • générer toutes les clauses (i=0, ..., n) pour sk := ak ⊕ bk ⊕ ck et ck+1 := (ak ∧ bk) ∨ (ak ∧ ck) ∨ (bk ∧ ck)

    • imposer les valeurs de s0, s1, ... pour obtenir le résultat s.

    • s0 correspond à la parité de s,

    • s1 correspond à la parité de s // 2,

    • s2 correspond à la parité de s // 4,

    • s3 correspond à la parité de s // 8,

    • etc.

  2. Vérifiez que vous obtenez une solutions correcte pour l'addition a + b == 7, sur 4 bits.

    Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.

    Pour ceci, vous pouvez lancer le programme avec 2 arguments : la valeur de n (nombre de bits) et la valeurs de s (somme voulue).

    $ ./addition-NOM.py 4 7 > add-7.sat

    permettra de générer les clauses correspondantes dans le fichier add-7.sat.

Si on ne donne pas de nom de fichier pour la formule, naivesat-NOM.py la lit sur l'entrée standard. Il est donc possible d'utiliser les redirections des shells Unix pour éviter d'avoir à sauvegarder un fichier à chaque fois

$ ./addition-NOM.py 4 7 | ./naivesat-NOM.py -v
>>> the formula contains 17 clauses and 70 variables, for a total of 262 literals
SATISFIABLE
~r1 ~r5 s1 s2 s3 ~s4 a1 ~b1 ~r2 a2 ~b2 ~r3 a3 ~b3 ~r4 ~a4 ~b4

Testez (sur des petites valeurs) en utilisant la fonction all_solutions pour vérifiez que vous trouvez bien le bon nombre de solutions pour a + b == s. (Il y a exactement s+1 manières d'obtenir s comme la somme de 2 nombres : 0+s, 1+(s-1), 2+(s-2), ..., s + 0.)

Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.

Inverser la multiplication

Inverser un circuit additionneur n'est pas très intéressant. Par contre, inverser un circuit multiplicateur l'est plus. Vérifier la satisfiabilité correspondant à a × b = p (pour un p donné) revient à chercher des facteurs de p, càd à factoriser p !

Générer les clauses correspondant à une multiplication est un peu plus complexe que pour l'addition. Le fichier multiplication-NOM.py le fera pour vous.

  1. Ajoutez, tout à la fin du fichier, les clauses pertinentes pour forcer les valeurs de p0, p1, ... p_n aux bits de p. (C'est identique à ce que vous avez fait pour forcer )

    Vérifiez que vous arrivez bien à factoriser le nombre 48 en 2 nombres de 4 bits :

    $ ./ multiplication.py 4 4 48 | ./naivesat-NOM.py -r
    SATISFIABLE
    x_17 ~x_13 ~x_15 ~a_2 ~q_2_2 p_5 ~p_6 ~q_2_1 ~q_1_2 ~x_8 ~q_1_0 ~x_1 ~x_9 ~q_3_0 ~x_6 ~q_2_0 ~q_2_3 b_1 ~q_1_1 ~a_1 x_12 ~q_0_2 a_3 ~x_10 ~x_4 ~b_0 ~q_0_0 ~x_16 ~x_18 ~x_20 ~a_0 p_4 ~q_1_3 ~x_19 ~q_3_3 ~x_2 ~x_7 ~q_0_1 ~x_11 q_3_2 ~p_2 ~x_5 ~q_0_3 ~p_3 ~x_14 q_3_1 ~x_3 ~p_0 ~p_7 ~p_1 ~b_3 b_2

    Attention, il y a plusieurs solutions (la factorisation n'est pas unique) et vous n'obtiendrez pas forcément le même résultat. Vous devrez donc extraire les variables a_i et b_j de la solution pour retrouver les valeurs de a et b

  2. Vérifiez que pour les clauses correspondant à un nombre premier ne sont pas satisfiable.

    Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.

  1. À partir que quelles valeurs de n1 et n2 est-ce que la naivesat-NOM.py devient trop lent (résultat en 30 secondes environ) ?

    À combien de variables / clauses est-ce que cela correspond ?

  2. Testez les mêmes fichiers avec le programme minisat (paquet Ubuntu / Debian minisat), un SAT solveur très optimisé écrit en C++. À partir de quelles valeurs de n1 et n2 est-ce que minisat devient trop lent ?

    minisat lit la formule dans un fichier au format DIMACS. naivesat-NOM.py peut sauvegarder une formule au format DIMACS avec l'option -O / --output:

    $ ./multiplication.py 10 10 179 | ./naivesat-NOM.py -O "" formule.cnf
    $ minisat formule.cnf

    Pour les experts du shell, vous pouvez même utiliser les "substitutions de processus" pour éviter d'avoir passer par un fichier :

    $ minisat <(./multiplication.py 10 10 179 | ./naivesat-NOM.py -O)

    Pour obtenir la solution trouvée, il faut fournir un second nom de fichier à minisat pour qu'il y écrive le résultat :

    $ ./multiplication.py 10 10 179 | ./naivesat-NOM.py -O "" formule.cnf
    $ minisat formule.cnf formule.sol

    Pour éviter de passer par un fichier, vous pouvez faire une autre substitution de processus :

    $ minisat <(./multiplication.py 10 10 179 | ./naivesat-NOM.py -O) >(cat)

2. Optimisation du solveur naïf : algorithme DPLL

La première avancée sur les solveur a été l'algorithme DPLL. L'idée principale est d'ajouter une phase de simplification avant chaque choix du solveur. Cette étape est parfois appelée BCP, pour Boolean Constraint Propagation.

L'algorithme DPLL proposait 2 simplifications :

En pratique, seule la première simplification est importante. La seconde demande en général trop de calculs.

La propagation des clauses unitaires consiste à identifier les clauses avec un unique littéral dans la formule :

... ∧ ... ∧ (L) ∧ ... ∧ ...

Pour satisfaire cette formule, le littéral L doit forcément être à vrai. On peut donc l'ajouter à la solution courante et simplifier la formule avec la fonction simplify_formula écrite précédemment.

Bien entendu, il faut faire cette simplification autant de fois que nécessaire.

  1. Ajoutez les quelques lignes suivantes au tout début de la fonction solve, juste après la docstring :

        # boolean constraints propagation
        if bcp:
            F, sol2 = BCP(F)
            sol.extend(sol2)
    

    Ceci permettra d'appeler automatique la fonction de simplification à chaque étape.

  2. Complétez le corps de la fonction BCP(F). Cette fonction doit renvoyer 2 choses :

    • la formule simplifié au maximum,

    • la liste des littéraux qui ont servi à cette simplification.

    Pour information, le corps de ma fonction fait 12 lignes, en comptant les 2 lignes "sol = []" et "return F, sol".

  3. Pour tester, vous pouvez utiliser l'option -D / --DPLL de naivesat-NOM.py.

    Décrivez les tests que vous effectuez et comparez l'efficacité du solveur optimisé avec le solveur non optimisé.

  4. Comparez l'efficacité du solveur naif optimisé avec minisat et documentez vos résultats.

Testez d'autres optimisations comme :