Modéliser un problème en SAT
Ce que ce chapitre apporte
- Suivre une méthode de modélisation en quatre étapes, et savoir où chacune peut échouer.
- Encoder « au moins un », « au plus un », « exactement un » et en comparer les coûts.
- Traduire une coloration de graphe, un sudoku et un emploi du temps en clauses.
- Reconnaître une contrainte de cardinalité et choisir un encodage adapté à sa taille.
- Repérer les symétries d'un problème et savoir ce qu'elles coûtent.
- Retraduire un modèle rendu par le solveur, et le contrôler contre l'énoncé.
- Diagnostiquer un UNSAT inattendu, et savoir ce qu'est un noyau insatisfiable.
- Vérifier une modélisation autrement qu'en la relisant.
La méthode, en quatre étapes
2. Écrire les contraintes une par une, en langue naturelle d'abord, puis en clauses. Une contrainte de l'énoncé, un paragraphe de clauses.
3. Vérifier sur deux cas : une solution valide doit satisfaire la formule, une configuration interdite doit la violer.
4. Relire le modèle rendu par le solveur et le retraduire dans le problème. Une solution qu'on ne sait pas relire est une solution qu'on ne sait pas vérifier.
Il ne dit rien, en revanche, sur la question de savoir si la formule décrit le problème. Une contrainte oubliée donne une réponse parfaitement correcte à une question qui n'était pas la bonne, et rien dans la sortie ne le signale. C'est la seule source d'erreur du domaine, et elle est entièrement humaine.
L'encodage direct
C'est le point de départ de presque toutes les modélisations : une variable par couple objet, valeur.
Pour affecter à chacun de objets une valeur parmi , on pose variables :
vaut 1 si et seulement si l'objet reçoit la valeur .
Il faut alors imposer explicitement que chaque objet reçoive exactement une valeur : rien dans les variables ne l'exprime.
Oublier les clauses « exactement une valeur » est la faute de modélisation la plus fréquente, et elle produit des solutions absurdes que le solveur défend pourtant très bien.
Les contraintes de cardinalité
Elles reviennent partout : au moins un, au plus un, exactement , au plus .
Au moins un parmi : une seule clause, .
Au plus un, encodage par paires : pour chaque paire , soit clauses et aucune variable nouvelle.
L'encodage par paires est simple et parfait pour de petites valeurs de . Il devient déraisonnable quand grandit : pour , il produit près de cinq cent mille clauses pour une seule contrainte.
L'idée de l'encodage suivant est de faire circuler un signal. Les variables auxiliaires forment une chaîne de relais : signifie « un des vaut déjà 1 ». Trois règles suffisent alors : allumer un allume le relais , un relais allumé reste allumé plus loin dans la chaîne, et il est interdit d'allumer un si le relais précédent l'est déjà.
On introduit variables auxiliaires, signifiant « un des vaut déjà 1 ». Les clauses sont alors :
, et pour
soit clauses et variables : linéaire au lieu de quadratique.
Poser . La clause devient unitaire et allume . La clause devient alors unitaire et allume , puis , et ainsi de suite jusqu'au bout de la chaîne.
Tentons maintenant . La clause interdit cette combinaison, puisque est allumé : la clause est falsifiée, et le solveur le voit sans avoir rien à décider.
C'est ce parcours de proche en proche qui remplace les clauses de l'encodage par paires. Le prix est un délai : là où l'encodage par paires interdit immédiatement, le séquentiel doit d'abord propager le long de la chaîne.
Au-delà : séquentiel, ou l'un de ses raffinements. Le nombre de clauses devient le facteur limitant, et les variables auxiliaires se paient largement.
Le seuil exact dépend de l'instance, et il se mesure au lieu de se deviner : les deux encodages tiennent en quelques lignes de code, et essayer les deux coûte moins cher que d'en discuter.
L'encodage par paires a cette propriété. Poser rend le littéral faux ; la clause ne tient donc plus qu'à son second littéral, elle est unitaire, et elle force . Il en va de même pour toutes les autres, en une seule passe de propagation. L'encodage séquentiel conserve la propriété lui aussi, à travers la chaîne des relais.
Un encodage plus court mais qui ne propage pas est presque toujours un mauvais marché : le solveur devra deviner ce qu'il aurait pu déduire.
Colorer un graphe
Les variables. vaut 1 si et seulement si le sommet reçoit la couleur . Avec 5 sommets et 3 couleurs, cela fait 15 variables.
Chaque sommet a au moins une couleur. Une clause par sommet : .
Chaque sommet a au plus une couleur. Trois clauses par sommet, par paires de couleurs.
Deux sommets adjacents diffèrent. Pour chaque arête et chaque couleur : . Soit trois clauses par arête.
Voici, pour le seul triangle , les clauses que produit cet encodage. Cliquer sur les variables permet de constater qu'aucune couleur ne peut être partagée.
- c1(Arouge∨Avert∨Ableu)
- c2(Brouge∨Bvert∨Bbleu)
- c3(Crouge∨Cvert∨Cbleu)
- c4(¬Arouge∨¬Brouge)⇒ Brouge = 0
- c5(¬Avert∨¬Bvert)⇒ Avert = 0
- c6(¬Ableu∨¬Bbleu)
- c7(¬Arouge∨¬Crouge)⇒ Crouge = 0
- c8(¬Avert∨¬Cvert)
- c9(¬Ableu∨¬Cbleu)
Propagation unitaire : Brouge = 0, Avert = 0, Crouge = 0. Ces valeurs ne sont pas des choix, elles sont imposées.
Pour la question « énumérer les colorations », oui : sans elles, une même coloration est comptée plusieurs fois.
La leçon générale : ce qu'un encodage peut omettre dépend de la question posée, pas du problème. Le chapitre 10 y revient, à propos du comptage de modèles.
Le sudoku, en entier
C'est l'exemple canonique parce qu'il est entièrement fait de contraintes « exactement un ».
Les variables. vaut 1 si et seulement si la case en ligne , colonne contient la valeur . Soit variables.
Les contraintes, toutes de la même forme :
- chaque case contient exactement une valeur : 81 contraintes sur 9 variables ;
- chaque ligne contient chaque valeur exactement une fois : 81 contraintes ;
- chaque colonne, de même : 81 contraintes ;
- chaque bloc de 3 sur 3, de même : 81 contraintes.
Les indices de la grille de départ deviennent des clauses unitaires : une case donnée à 7 impose .
En encodage par paires, cela fait environ 11 000 clauses. Un solveur moderne résout une telle instance en quelques millisecondes, et il passe l'essentiel de ce temps en propagation unitaire, sans presque jamais décider.
C'est exactement ce que fait un joueur humain quand il raisonne « il ne reste qu'une case possible pour le 4 dans ce bloc ». La différence est que le solveur le fait un million de fois par seconde, et qu'il sait revenir en arrière quand il a dû parier.
Un emploi du temps
Les variables. vaut 1 si le cours se tient en salle au créneau . Ici variables.
Chaque cours a lieu exactement une fois : pour chaque , exactement une des 8 variables vaut 1.
Une salle accueille au plus un cours par créneau : pour chaque couple , au plus un des vaut 1. Huit contraintes « au plus un » sur trois variables.
Un enseignant ne se dédouble pas : si les cours 1 et 2 ont le même enseignant, alors pour chaque créneau , au plus un des et vaut 1, toutes salles confondues.
Une préférence, si on veut : « le cours 3 n'est pas au dernier créneau » se dit par des clauses unitaires .
Deux issues honnêtes. Soit on durcit la préférence en contrainte stricte, et le problème peut devenir insatisfiable. Soit on change d'outil pour MaxSAT, qui cherche à satisfaire le plus grand nombre de clauses souples, et que le chapitre 10 présente.
Ce qu'il ne faut pas faire, c'est encoder une préférence comme une contrainte dure sans le dire : on obtient un UNSAT incompréhensible, sur un problème qui avait des solutions acceptables.
Écrire les clauses par programme
Passé une dizaine de variables, écrire les clauses à la main n'a plus de sens : une contrainte « au plus un » sur 40 variables en compte 780. Personne ne tape cela au clavier, et c'est précisément pourquoi le format DIMACS est fait pour être écrit par un programme.
exactement_un se teste une fois pour toutes, sur trois variables, et sert ensuite sur quarante sans risque d'oubli.Il rend aussi le coût visible : la dernière boucle affiche le nombre de clauses produites, et l'on voit à quel moment l'encodage par paires devient déraisonnable. C'est le genre de mesure qui remplace avantageusement une discussion.
Relire la réponse
C'est la quatrième étape de la méthode, et celle qu'on saute. Un solveur ne rend pas une coloration : il rend une liste de littéraux. La retraduire fait partie de la modélisation, et c'est aussi le seul moment où l'on peut vérifier qu'on a modélisé ce qu'on croyait.
numero sert dans les deux sens : elle produit les clauses, et elle relit le modèle. Une seule définition, donc aucune chance qu'ils divergent.Le contrôle final, lui, ne regarde pas la formule : il vérifie la solution contre l'énoncé, arête par arête. C'est le seul moyen d'attraper une contrainte oubliée, puisqu'une contrainte absente de la formule est aussi absente du raisonnement du solveur.
Vingt lignes, écrites une fois, qui transforment un « le solveur dit SAT » en « voici une solution, et elle est correcte ».
Quand le solveur répond UNSAT et qu'on ne comprend pas pourquoi
C'est la situation la plus fréquente en modélisation réelle, et la plus déroutante : le problème a manifestement des solutions, et le solveur affirme le contraire. Il a raison sur la formule ; c'est la formule qui est fausse.
2. Retirer les contraintes une à une. Commenter un groupe de clauses, relancer. Dès que la réponse redevient SAT, le groupe retiré contient l'erreur.
3. Demander un noyau insatisfiable. La plupart des solveurs savent rendre un sous-ensemble de clauses déjà insatisfiable à lui seul. C'est l'étape 2, faite automatiquement et beaucoup mieux.
Un noyau insatisfiable est un sous-ensemble des clauses qui est déjà insatisfiable.
Il est minimal si en retirer n'importe quelle clause le rend satisfiable.
La formule ci-dessous compte cinq clauses et se révèle insatisfiable. Trois d'entre elles suffisent pourtant à l'expliquer.
Poser : la clause force , et force . Poser : la clause est falsifiée. Aucune issue.
Le noyau est donc . Les deux autres clauses, qui portent sur et , n'y sont pour rien : elles pourraient décrire la moitié du problème sans changer le verdict.
Sur une formule de dix mille clauses, cette réduction est la différence entre une relecture aveugle et un diagnostic.
- c1(x)⇒ x = 1
- c2(¬x∨y)
- c3(¬x∨¬y)
- c4(z∨w)
- c5(¬z∨w)
Propagation unitaire : x = 1. Ces valeurs ne sont pas des choix, elles sont imposées.
Cela change la façon de travailler. Au lieu de recompiler un fichier par variante, on garde une seule formule en mémoire et on relance avec des hypothèses différentes. Le solveur conserve d'un appel à l'autre tout ce qu'il a appris, et les appels suivants sont bien plus rapides que le premier.
Et quand une résolution sous hypothèses échoue, le solveur rend le sous-ensemble des hypothèses responsable de l'échec, ce qui est un noyau insatisfiable ciblé sur ce qu'on voulait tester.
La question à trancher est donc : le modèle est-il faux, ou l'énoncé est-il impossible ? Le noyau insatisfiable répond aux deux, puisqu'il nomme les contraintes en cause. Il ne reste qu'à demander à qui a écrit l'énoncé laquelle il accepte de relâcher.
Les symétries
Une symétrie d'une instance est une permutation des variables qui transforme toute solution en une autre solution.
Dans la coloration, permuter les noms des couleurs est une symétrie : toute solution en engendre autres, identiques au renommage près. Dans l'emploi du temps, deux salles interchangeables en engendrent deux fois plus.
Sur une instance insatisfiable, elles sont un désastre. Le solveur doit réfuter chaque configuration, et il refait fois le même raisonnement sur des variables aux noms différents, sans jamais s'apercevoir qu'il se répète.
C'est la raison pour laquelle certaines instances minuscules résistent des heures, alors qu'un humain voit la réponse en une phrase.
Pourquoi cela ne perd aucune solution. Les noms des couleurs sont interchangeables. S'il existe une coloration valide où le sommet 1 est vert, on peut renommer partout « vert » en « couleur 1 » : les arêtes restent respectées, puisque le renommage est le même dans tout le graphe. Toute solution possède donc une copie qui satisfait les clauses ajoutées, et il suffit de chercher celle-là.
La formule reste équisatisfiable, et l'espace de recherche est divisé par . C'est l'une des interventions au meilleur rapport entre l'effort et le gain.
Le problème des pigeons
Placer pigeons dans casiers, un seul pigeon par casier. C'est évidemment impossible.
Variables : vaut 1 si le pigeon est dans le casier .
Clauses : chaque pigeon est quelque part, ; et deux pigeons ne partagent pas un casier, .
Un solveur fondé sur la résolution, c'est-à-dire tous les solveurs CDCL actuels, a besoin d'un nombre exponentiel d'étapes. Ce n'est pas un défaut d'implémentation : c'est un théorème, dû à Haken en 1985, et il vaut pour toute preuve par résolution, quelle qu'elle soit.
, avec 13 pigeons et 12 casiers, soit 156 variables, met déjà en difficulté des solveurs qui traitent par ailleurs des instances d'un million de variables. C'est l'exemple à garder en tête chaque fois qu'on est tenté de croire qu'un petit problème est un problème facile.
1.Pour placer 8 reines sur un échiquier de 8 sur 8, combien de variables dans un encodage direct « une case, une valeur » ?
2.Un modèle de coloration oublie les clauses « chaque sommet a au moins une couleur ». Que rend le solveur ?
3.Ajouter des clauses de rupture de symétrie rend l'instance…
Exercices type
Combien de clauses pour « exactement un parmi 10 variables », en encodage par paires ?
Au moins un : 1 clause de 10 littéraux.
Au plus un : clauses de 2 littéraux.
Total : 46 clauses, aucune variable nouvelle.
En encodage séquentiel : clauses, plus la clause « au moins un », mais aussi 9 variables auxiliaires. À cette taille, l'encodage par paires reste le meilleur choix : 46 clauses binaires sur lesquelles le solveur propage sans effort valent mieux que 27 clauses et 9 variables de plus.
Encoder « au plus deux parmi »
La généralisation directe de l'encodage par paires : interdire tous les sous-ensembles de trois variables.
Il y a tels sous-ensembles, d'où quatre clauses :
, , , .
En général, « au plus parmi » demande clauses par cette méthode, ce qui n'est praticable que pour de petites valeurs. Au-delà, on emploie un encodage à variables auxiliaires, du même esprit que le séquentiel, souvent construit sur un réseau de tri.
Une coloration à 2 couleurs d'un triangle est-elle satisfiable ? Le montrer sur les clauses
Trois sommets , , , deux couleurs. Variables .
Au moins une couleur : , , .
Arêtes : , , et de même pour les paires et .
Supposons . Les clauses d'arête forcent et , donc par « au moins une » on obtient et . Mais l'arête contient , qui est alors falsifiée.
Le cas est symétrique. La formule est donc insatisfiable, ce qui traduit exactement le fait qu'un triangle n'est pas biparti.
Noter que le raisonnement ci-dessus est mené uniquement par propagation unitaire, après une seule décision. C'est très exactement ce que fera DPLL au chapitre 7.
Pourquoi ajouter des clauses peut-il accélérer un solveur ?
Parce que le coût d'un solveur n'est pas dans le nombre de clauses, mais dans le nombre de décisions qu'il doit prendre et défaire.
Une clause supplémentaire qui rend une propagation possible remplace une décision, donc coupe une branche entière de la recherche. Les clauses de rupture de symétrie en sont l'exemple type : elles alourdissent la formule et divisent le travail.
C'est aussi tout le principe de CDCL, au chapitre 8 : le solveur ajoute lui-même des clauses pendant la recherche, chacune résumant une impasse qu'il ne refera plus.
La méthode
- Écrire le sens de chaque variable en français avant d'écrire une seule clause.
- Traiter chaque contrainte de l'énoncé séparément, et la nommer.
- Ne jamais oublier les clauses « exactement un » : les variables ne les portent pas.
- Compter les clauses d'une contrainte de cardinalité avant de l'écrire, pour choisir l'encodage.
- Préférer un encodage qui propage à un encodage seulement plus court.
- Chercher les symétries, et les casser quand l'instance risque d'être insatisfiable.
- Vérifier la modélisation sur deux cas, un valide et un interdit, avant de lancer le solveur.
Synthèse
- Le solveur ne se trompe jamais ; la traduction, si. C'est la seule source d'erreur.
- L'encodage direct pose une variable par couple objet-valeur, et n'impose rien tout seul.
- « Au moins un » est une clause ; « au plus un » par paires coûte clauses.
- L'encodage séquentiel de « au plus un » coûte clauses et variables.
- Un bon encodage se juge à ce qu'il fait propager, pas seulement à sa taille.
- Colorer un graphe : au moins une couleur par sommet, et une clause par arête et par couleur.
- Une contrainte souple n'a pas de place en SAT : la durcir, ou passer à MaxSAT.
- Les symétries multiplient le travail sur les instances insatisfiables ; les casser coûte peu.
- Le décodeur s'écrit en même temps que l'encodeur, et le contrôle final regarde l'énoncé, pas la formule.
- Devant un UNSAT inattendu : ajouter une solution connue, retirer les contraintes une à une, demander un noyau insatisfiable.
- Les hypothèses permettent de tester des variantes sans recompiler ni perdre ce que le solveur a appris.
- est évident pour un humain et exponentiel pour toute preuve par résolution.