#r "nuget: Microsoft.Z3"
#load "../SymbolicAI/SMT/Z3-API/Z3NativeLoader.cs"
// Bibliotheque native de Z3 hors Windows et macOS Intel (voir Z3NativeLoader.cs)
Z3NativeLoader.Register(typeof(Microsoft.Z3.Context).Assembly);- Microsoft.Z3, 4.12.2
Navigation : << Sudoku-11 Choco C# | Index | Sudoku-13 Symbolic Automata C# >>
À la fin de ce notebook, vous saurez :
Z3 est un SMT solver (Satisfiability Modulo Théories) qui peut être utilisé pour résoudre des problèmes de logique du premier ordre. Il permet de vérifier la satisfiabilité d’une formule logique sous certaines contraintes et est particulièrement efficace pour résoudre des problèmes impliquant des contraintes complexes, comme les puzzles de Sudoku.
Un SMT solver combine des techniques de résolution SAT (Satisfiability) avec des théories comme l’arithmétique, les tableaux, les bit-vectors, etc., permettant ainsi de résoudre des problèmes logiques beaucoup plus riches.
Références : - Exemples en C# - Programming Z3 - Z3Py Guide Examples en Python
La cellule suivante restaure le package NuGet Microsoft.Z3. Sous Linux et sous macOS Apple Silicon, ce package ne contient pas la bibliothèque native de Z3 : la cellule charge alors celle du paquet Python z3-solver, par Z3NativeLoader.cs de la série Z3-API (voir l’en-tête du fichier). Pour utiliser la même version de Z3 que le package NuGet : pip install z3-solver==4.12.2.0.
Nous allons importer les classes de base définies dans le notebook précédent, fournissant notamment la représentation, le chargement et l’affichage de Sudokus, et l’infrastructure de résolution.
Navigation : Index | Sudoku-01 Backtracking C# >>
À la fin de ce notebook, vous saurez : 1. Comprendre la structure de données SudokuGrid et ses méthodes principales 2. Utiliser ISudokuSolver pour implémenter un solveur de Sudoku 3. Exploiter SudokuHelper pour charger des grilles et tester des solveurs 4. Comparer les performances de plusieurs solveurs sur différentes difficultés
Prérequis : Notions de base en C# (.NET Interactive)
Durée estimée : ~15 min
Nous définissons ici la classe SudokuGrid qui représente une grille de Sudoku et fournit des méthodes pour manipuler et afficher les grilles.
SudokuGrid defini.
Sortie obtenue : La classe SudokuGrid encapsule toutes les opérations de manipulation, validation et affichage d’une grille de Sudoku 9x9.
| Aspect | Valeur | Signification |
|---|---|---|
Cells[9,9] |
int[,] | Stockage interne des valeurs (0 = vide) |
AllNeighbours |
27 x 9 positions | Pré-calcul des voisins ligne/colonne/bloc |
CellNeighbours[9][9] |
~20 positions chacune | Voisins directs de chaque cellule |
GetAvailableNumbers() |
int[] | Candidats valides pour une cellule |
NbErrors() |
int | Nombre de conflits + modifications erronées |
Points clés : 1. Pré-calcul des voisins : AllNeighbours et CellNeighbours sont calculés une seule fois à l’initialisation, évitant les recalculs coûteux 2. Conversion flexible : Méthodes pour convertir entre tableaux 1D, 2D et jagged arrays (utile pour différents formats de fichiers) 3. Validation robuste : NbErrors compte à la fois les doublons (ligne/colonne/bloc) et les modifications de indices pré-remplis 4. Parsing tolerant : ReadMultiSudoku accepte plusieurs formats (., X, -, espaces)
Note technique : La structure
CellNeighbours[i][j]contient environ 20 positions (8 ligne + 8 colonne + 4 bloc, moins les doublons). Ce pré-calcul est crucial pour les performances des algorithmes de backtracking et de propagation de contraintes.
Nous définissons ici l’interface ISudokuSolver qui sera implémentée par les différentes stratégies de résolution de Sudoku.
ISudokuSolver defini.
Sortie obtenue : L’interface ISudokuSolver définit le contrat que tous les solveurs doivent respecter.
| Aspect | Valeur | Signification |
|---|---|---|
Solve(SudokuGrid) |
SudokuGrid | Méthode unique de résolution |
| Pattern | Stratégie | Permuter les algorithmes sans modifier le code client |
Points clés : 1. Simplicité : Une seule méthode Solve prenant une grille et retournant une grille résolue 2. Flexibilité : N’importe quel algorithme (backtracking, CSP, métaheuristique) peut implémenter cette interface 3. Composabilité : Les solveurs peuvent être passés en paramètre, stockés dans des listes, testés unitairement 4. Extensibilité : Ajouter un nouveau solver ne nécessite que d’implémenter l’interface
Note technique : Ce design pattern permet à
SudokuHelper.TestSolversd’accepter une liste de(string, ISudokuSolver)pour comparer tous les algorithmes avec le même code de test.
Nous ajoutons ici la classe SudokuHelper qui contient des méthodes utilitaires pour charger des grilles de Sudoku et tester des solvers.
GetSudokus : Renvoie des listes de Sudoku issues de fichiers de 3 difficultés différentes.SolveSudoku : effectue un test simple d’un solver sur un sudoku donné.TestSolvers : exécute les tests de performance sur plusieurs solveurs.DisplayResults : affiche les résultats des tests sous forme de graphiques.SudokuHelper defini.
Sortie obtenue : La classe SudokuHelper fournit une infrastructure complète pour tester et comparer les solveurs de Sudoku.
| Aspect | Valeur | Signification |
|---|---|---|
GetSudokus() |
51/95/100 grilles | Trois niveaux de difficulté (Easy/Medium/Hard) |
TestSolvers() |
Performance multi-solveurs | Exécution parallèle avec timeout |
DisplayResults() |
Graphiques SVG inline (SvgChartHelper) | Comparaison des temps par difficulté, sérialisée dans le notebook |
SolveSudoku() |
Test unitaire | Résolution individuelle avec affichage |
Points clés : 1. Chargement intelligent : Recherche récursive du dossier Puzzles dans l’arborescence 2. Robustesse : Gestion des timeouts (3 000 ms par défaut — paramètre de configuration du solveur, valeur fixée dans le code) et exceptions 3. Mesures : Temps d’exécution total + nombre de grilles resolues 4. Disqualification : Un solver échouant sur une grille est disqualifié pour la difficulté
Note technique : La méthode
TestSolversutiliseInterlocked.Incrementpour un thread-safe incrément du compteur de solutions. LeCancellationTokenpermet d’interrompre proprement les solveurs trop lents.
Implémentez une méthode IsValidSolution qui vérifie qu’une grille est une solution valide de Sudoku, c’est-à-dire que chaque ligne, chaque colonne et chaque bloc 3x3 contient exactement une fois chaque chiffre de 1 à 9.
Utilisez cette méthode pour valider les résultats de SudokuHelper.SolveSudoku.
Indices :
SudokuGrid.AllNeighbours contient déjà les indices des unitésExercice a completer
Ce notebook a posé les fondations de toute la série Sudoku en définissant trois composants essentiels. La classe SudokuGrid encapsule la représentation d’une grille 9x9 avec le pré-calcul des voisins (AllNeighbours, CellNeighbours), ce qui évite les recalculs coûteux lors de la résolution. L’interface ISudokuSolver implante le pattern Stratégie, permettant de permuter les algorithmes de résolution sans modifier le code client. Enfin, la classe SudokuHelper fournit une infrastructure de benchmark complète avec chargement de puzzles, mesures de performance et visualisation SVG inline (SvgChartHelper, zéro dépendance).
L’infrastructure de test (TestSolvers, DisplayResults) permet de comparer objectivement les solveurs sur trois niveaux de difficulté (Easy, Medium, Hard) avec gestion des timeouts et des disqualifications. Ce cadre de benchmark sera utilisé dans tous les notebooks suivants pour mesurer les performances de chaque algorithme.
Le notebook suivant, Sudoku-01-Backtracking, utilise ces classes pour implémenter le premier algorithme de résolution : le backtracking récursif avec ses heuristiques d’amélioration.
Comme nous allons tester plusieurs stratégies de résolution, nous commencerons par implémenter un solver simple utilisant des entiers pour représenter les cellules du Sudoku et fournissant les méthodes pour construire les contraintes.
// Importer les bibliothèques nécessaires
using System.Diagnostics;
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Z3;
// Classe pour résoudre le Sudoku en utilisant Z3 avec des entiers
public class Z3IntSolverSimple : ISudokuSolver
{
public static Context ctx = new Context();
public static BoolExpr _GenericContraints;
public static IntExpr[][] CellVariables = new IntExpr[9][];
public Z3IntSolverSimple()
{
// Initialiser les variables de cellule
for (uint i = 0; i < 9; i++)
{
CellVariables[i] = new IntExpr[9];
for (uint j = 0; j < 9; j++)
CellVariables[i][j] = (IntExpr)ctx.MkConst(ctx.MkSymbol("x_" + (i + 1) + "_" + (j + 1)), ctx.IntSort);
}
}
// Contraintes génériques pour tous les Sudokus, conservées en mémoire pour éviter de les recalculer
public static BoolExpr GenericContraints
{
get
{
if (_GenericContraints == null)
{
_GenericContraints = GetGenericConstraints();
}
return _GenericContraints;
}
}
// Générer les contraintes génériques
public static BoolExpr GetGenericConstraints()
{
// Chaque cellule contient une valeur entre 1 et 9
Expr[][] cells_c = new Expr[9][];
for (uint i = 0; i < 9; i++)
{
cells_c[i] = new BoolExpr[9];
for (uint j = 0; j < 9; j++)
cells_c[i][j] = ctx.MkAnd(ctx.MkLe(ctx.MkInt(1), CellVariables[i][j]),
ctx.MkLe(CellVariables[i][j], ctx.MkInt(9)));
}
// Chaque ligne contient des chiffres distincts
BoolExpr[] rows_c = new BoolExpr[9];
for (uint i = 0; i < 9; i++)
rows_c[i] = ctx.MkDistinct(CellVariables[i]);
// Chaque colonne contient des chiffres distincts
BoolExpr[] cols_c = new BoolExpr[9];
for (uint j = 0; j < 9; j++)
{
IntExpr[] column = new IntExpr[9];
for (uint i = 0; i < 9; i++)
column[i] = CellVariables[i][j];
cols_c[j] = ctx.MkDistinct(column);
}
// Chaque carré 3x3 contient des chiffres distincts
BoolExpr[][] sq_c = new BoolExpr[3][];
for (uint i0 = 0; i0 < 3; i0++)
{
sq_c[i0] = new BoolExpr[3];
for (uint j0 = 0; j0 < 3; j0++)
{
IntExpr[] square = new IntExpr[9];
for (uint i = 0; i < 3; i++)
for (uint j = 0; j < 3; j++)
square[3 * i + j] = CellVariables[3 * i0 + i][3 * j0 + j];
sq_c[i0][j0] = ctx.MkDistinct(square);
}
}
BoolExpr sudoku_c = ctx.MkTrue();
foreach (BoolExpr[] t in cells_c)
sudoku_c = ctx.MkAnd(ctx.MkAnd(t), sudoku_c);
sudoku_c = ctx.MkAnd(ctx.MkAnd(rows_c), sudoku_c);
sudoku_c = ctx.MkAnd(ctx.MkAnd(cols_c), sudoku_c);
foreach (BoolExpr[] t in sq_c)
sudoku_c = ctx.MkAnd(ctx.MkAnd(t), sudoku_c);
return sudoku_c;
}
// Générer les contraintes spécifiques à une grille de Sudoku donnée
public BoolExpr GetPuzzleConstraints(SudokuGrid grid)
{
BoolExpr instance_c = ctx.MkTrue();
for (uint i = 0; i < 9; i++)
for (uint j = 0; j < 9; j++)
if (grid.Cells[i,j] != 0)
{
instance_c = ctx.MkAnd(instance_c,
(BoolExpr)ctx.MkEq(CellVariables[i][j], ctx.MkInt(grid.Cells[i,j])));
}
return instance_c;
}
// Résoudre le Sudoku
public SudokuGrid Solve(SudokuGrid grid)
{
SudokuGrid solution = new SudokuGrid();
var sudoku_c = GenericContraints;
var instance_c = GetPuzzleConstraints(grid);
Solver s = ctx.MkSolver();
s.Assert(sudoku_c);
s.Assert(instance_c);
if (s.Check() == Status.SATISFIABLE)
{
Model m = s.Model;
for (uint i = 0; i < 9; i++)
{
for (uint j = 0; j < 9; j++)
{
solution.Cells[i,j] = ((IntNum)m.Evaluate(CellVariables[i][j])).Int;
}
}
}
else
{
Console.WriteLine("Failed to solve sudoku");
throw new Exception("Failed to solve sudoku");
}
return solution;
}
}
Console.WriteLine("Classe Z3IntSolverSimple definie (solveur Z3 avec entiers)");Classe Z3IntSolverSimple definie (solveur Z3 avec entiers)
Avant de comparer les performances, vérifions que le solveur produit bien une grille valide. On charge une grille facile, on l’affiche (les 0 marquent les cases à remplir), puis on la résout avec Z3IntSolverSimple. La sortie montre la grille de départ, la grille complète, et le nombre d’erreurs restantes : 0 confirme une solution correcte.
// Demonstration : charger une grille réelle, la résoudre et comparer avant / après.
var puzzleDemo = SudokuHelper.GetSudokus(SudokuDifficulty.Easy)[0];
Console.WriteLine("Grille initiale (les 0 sont les cases a remplir) :");
Console.WriteLine(puzzleDemo);
var solvedInt = new Z3IntSolverSimple().Solve(puzzleDemo);
Console.WriteLine("Grille resolue par Z3IntSolverSimple :");
Console.WriteLine(solvedInt);
Console.WriteLine($"Nombre d'erreurs restantes (0 = grille valide) : {solvedInt.NbErrors(puzzleDemo)}");Grille initiale (les 0 sont les cases a remplir) :
-------------------------------
| 9 2 | 5 | 4 3 |
| 1 | 6 3 | 2 5 |
| 5 8 | 4 7 | 6 |
-------------------------------
| 2 6 | 3 9 | 1 |
| 5 7 | 1 | 2 9 |
| 9 | 6 7 | 5 3 |
-------------------------------
| 2 4 | 5 3 | 6 |
| 7 5 | 2 | 3 4 |
| 8 | 4 1 | 9 5 |
-------------------------------
Grille resolue par Z3IntSolverSimple :
-------------------------------
| 9 6 2 | 1 8 5 | 4 7 3 |
| 1 7 4 | 9 6 3 | 8 2 5 |
| 5 3 8 | 4 2 7 | 1 6 9 |
-------------------------------
| 8 2 6 | 3 5 9 | 7 4 1 |
| 3 5 7 | 8 1 4 | 2 9 6 |
| 4 9 1 | 6 7 2 | 5 3 8 |
-------------------------------
| 2 4 9 | 5 3 8 | 6 1 7 |
| 7 1 5 | 2 9 6 | 3 8 4 |
| 6 8 3 | 7 4 1 | 9 5 2 |
-------------------------------
Nombre d'erreurs restantes (0 = grille valide) : 0
Nous allons maintenant explorer une autre piste d’amélioration en utilisant des vecteurs de bits pour représenter les cellules du Sudoku. Cette approche peut réduire la taille des variables et améliorer les performances du solver.
Comme nous testerons plusieurs types de résolution, nous introduisons une classe de base qui fournit les contraintes.
using Microsoft.Z3;
public abstract class Z3BitVectorSolverBase : ISudokuSolver
{
public static Context ctx = new Context();
public static BoolExpr _GenericContraints;
public static BitVecExpr[][] CellVariables = new BitVecExpr[9][];
private static Sort BitVectorSort = ctx.MkBitVecSort(4);
public static Solver _ReusableSolver;
public Z3BitVectorSolverBase()
{
// Initialiser les variables de cellule en tant que vecteurs de 4 bits
for (uint i = 0; i < 9; i++)
{
CellVariables[i] = new BitVecExpr[9];
for (uint j = 0; j < 9; j++)
CellVariables[i][j] = (BitVecExpr)ctx.MkConst(ctx.MkSymbol("x_" + (i + 1) + "_" + (j + 1)), BitVectorSort);
}
}
// Contraintes génériques pour le Sudoku
public static BoolExpr GenericContraints
{
get
{
if (_GenericContraints == null)
{
_GenericContraints = GetGenericConstraints();
}
return _GenericContraints;
}
}
public static Solver ReusableSolver
{
get
{
if (_ReusableSolver == null)
{
_ReusableSolver = MakeReusableSolver();
}
return _ReusableSolver;
}
}
public static Solver MakeReusableSolver()
{
Solver s = ctx.MkSolver();
s.Assert(GenericContraints);
return s;
}
// Obtenir l'expression constante pour une valeur donnée
protected static BitVecExpr GetConstExpr(int value)
{
return (BitVecExpr)ctx.MkNumeral(value, BitVectorSort);
}
// Générer les contraintes génériques pour les vecteurs de bits
public static BoolExpr GetGenericConstraints()
{
// Chaque cellule contient une valeur entre 1 et 9
Expr[][] cells_c = new Expr[9][];
for (uint i = 0; i < 9; i++)
{
cells_c[i] = new BoolExpr[9];
for (uint j = 0; j < 9; j++)
cells_c[i][j] = ctx.MkAnd(ctx.MkBVULE(GetConstExpr(1), CellVariables[i][j]),
ctx.MkBVULE(CellVariables[i][j], GetConstExpr(9)));
}
// Chaque ligne contient des chiffres distincts
BoolExpr[] rows_c = new BoolExpr[9];
for (uint i = 0; i < 9; i++)
rows_c[i] = ctx.MkDistinct(CellVariables[i]);
// Chaque colonne contient des chiffres distincts
BoolExpr[] cols_c = new BoolExpr[9];
for (uint j = 0; j < 9; j++)
{
BitVecExpr[] column = new BitVecExpr[9];
for (uint i = 0; i < 9; i++)
column[i] = CellVariables[i][j];
cols_c[j] = ctx.MkDistinct(column);
}
// Chaque carré 3x3 contient des chiffres distinct
BoolExpr[][] sq_c = new BoolExpr[3][];
for (uint i0 = 0; i0 < 3; i0++)
{
sq_c[i0] = new BoolExpr[3];
for (uint j0 = 0; j0 < 3; j0++)
{
BitVecExpr[] square = new BitVecExpr[9];
for (uint i = 0; i < 3; i++)
for (uint j = 0; j < 3; j++)
square[3 * i + j] = CellVariables[3 * i0 + i][3 * j0 + j];
sq_c[i0][j0] = ctx.MkDistinct(square);
}
}
BoolExpr sudoku_c = ctx.MkTrue();
foreach (BoolExpr[] t in cells_c)
sudoku_c = ctx.MkAnd(ctx.MkAnd(t), sudoku_c);
sudoku_c = ctx.MkAnd(ctx.MkAnd(rows_c), sudoku_c);
sudoku_c = ctx.MkAnd(ctx.MkAnd(cols_c), sudoku_c);
foreach (BoolExpr[] t in sq_c)
sudoku_c = ctx.MkAnd(ctx.MkAnd(t), sudoku_c);
return sudoku_c;
}
// Générer les contraintes spécifiques à une grille de Sudoku donnée
public BoolExpr GetPuzzleConstraints(SudokuGrid grid)
{
BoolExpr instance_c = ctx.MkTrue();
for (uint i = 0; i < 9; i++)
for (uint j = 0; j < 9; j++)
if (grid.Cells[i,j] != 0)
{
instance_c = ctx.MkAnd(instance_c,
(BoolExpr)ctx.MkEq(CellVariables[i][j], GetConstExpr(grid.Cells[i,j])));
}
return instance_c;
}
// Méthode abstraite pour résoudre le Sudoku
public abstract SudokuGrid Solve(SudokuGrid s);
}
Console.WriteLine("Classe Z3BitVectorSolverBase definie (solveur Z3 abstrait avec vecteurs de bits)");Classe Z3BitVectorSolverBase definie (solveur Z3 abstrait avec vecteurs de bits)
Nous allons implémenter un solver simple en utilisant des vecteurs de bits.
Objectif : Ajoutez une contrainte de somme sur un bloc 3x3 pour modéliser un Killer Sudoku.
Indice : Utilisez MkAdd pour sommer les variables d’un bloc et MkEq pour fixer la somme attendue.
using Microsoft.Z3;
public class Z3BitVectorSolverSimple : Z3BitVectorSolverBase
{
public override SudokuGrid Solve(SudokuGrid s)
{
SudokuGrid solution = new SudokuGrid();
SudokuSolve(s, ref solution);
return solution;
}
public void SudokuSolve(SudokuGrid grid, ref SudokuGrid solution)
{
var sudoku_c = GenericContraints;
var instance_c = GetPuzzleConstraints(grid);
Solver s = ctx.MkSolver();
s.Assert(sudoku_c);
s.Assert(instance_c);
if (s.Check() == Status.SATISFIABLE)
{
Model m = s.Model;
for (uint i = 0; i < 9; i++)
{
for (uint j = 0; j < 9; j++)
{
solution.Cells[i,j] = ((BitVecNum)m.Evaluate(CellVariables[i][j])).Int;
}
}
}
else
{
Console.WriteLine("Failed to solve sudoku");
throw new Exception("Failed to solve sudoku");
}
}
}
Console.WriteLine("Classe Z3BitVectorSolverSimple definie (solveur Z3 BitVector simple)");Classe Z3BitVectorSolverSimple definie (solveur Z3 BitVector simple)
L’encodage précédent représente chaque case par un entier ; celui-ci la représente par un vecteur de bits de 4 bits. On résout la même grille facile pour vérifier que ce second encodage aboutit lui aussi à une solution valide.
// Demonstration : le même puzzle, résolu cette fois par l'encodage en vecteurs de bits.
var puzzleDemoBv = SudokuHelper.GetSudokus(SudokuDifficulty.Easy)[0];
Console.WriteLine("Grille initiale :");
Console.WriteLine(puzzleDemoBv);
var solvedBv = new Z3BitVectorSolverSimple().Solve(puzzleDemoBv);
Console.WriteLine("Grille resolue par Z3BitVectorSolverSimple :");
Console.WriteLine(solvedBv);
Console.WriteLine($"Nombre d'erreurs restantes (0 = grille valide) : {solvedBv.NbErrors(puzzleDemoBv)}");Grille initiale :
-------------------------------
| 9 2 | 5 | 4 3 |
| 1 | 6 3 | 2 5 |
| 5 8 | 4 7 | 6 |
-------------------------------
| 2 6 | 3 9 | 1 |
| 5 7 | 1 | 2 9 |
| 9 | 6 7 | 5 3 |
-------------------------------
| 2 4 | 5 3 | 6 |
| 7 5 | 2 | 3 4 |
| 8 | 4 1 | 9 5 |
-------------------------------
Grille resolue par Z3BitVectorSolverSimple :
-------------------------------
| 9 6 2 | 1 8 5 | 4 7 3 |
| 1 7 4 | 9 6 3 | 8 2 5 |
| 5 3 8 | 4 2 7 | 1 6 9 |
-------------------------------
| 8 2 6 | 3 5 9 | 7 4 1 |
| 3 5 7 | 8 1 4 | 2 9 6 |
| 4 9 1 | 6 7 2 | 5 3 8 |
-------------------------------
| 2 4 9 | 5 3 8 | 6 1 7 |
| 7 1 5 | 2 9 6 | 3 8 4 |
| 6 8 3 | 7 4 1 | 9 5 2 |
-------------------------------
Nombre d'erreurs restantes (0 = grille valide) : 0
L’encodage entier (cellule précédente Z3IntSolverSimple) et l’encodage vecteurs de bits (Z3BitVectorSolverSimple) produisent la même grille résolue, et les deux affichent Nombre d'erreurs restantes : 0. Cet accord n’est pas trivial : ce sont deux modélisations indépendantes de la même contrainte Sudoku.
IntVar ∈ {1,…,9} par case. La contrainte « tous différents » sur une ligne/colonne/bloc porte sur 9 entiers — naturelle à écrire, mais le solveur doit raisonner sur la théorie arithmétique.La convergence vers la solution identique est une preuve de cohérence entre les deux modélisations. Le banc d’essai comparatif ci-dessous (§6) tranchera ensuite sur le temps : l’encodage vecteur de bits y domine systématiquement, parce que le raisonnement bit à bit est plus directement exploitable par le moteur SAT interne de Z3 que la théorie des entiers.
Nous allons implémenter une classe utilisant l’API de substitution. Cette approche peut améliorer les performances en réutilisant les contraintes génériques et en substituant uniquement les valeurs spécifiques à la grille de Sudoku en cours de résolution.
using Microsoft.Z3;
public class Z3BitVectorSolverSubstitution : Z3BitVectorSolverBase
{
public override SudokuGrid Solve(SudokuGrid s)
{
SudokuGrid solution = new SudokuGrid();
SudokuSolve(s, ref solution);
return solution;
}
public void SudokuSolve(SudokuGrid grid, ref SudokuGrid solution)
{
var substExprs = new List<Expr>();
var substVals = new List<Expr>();
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++)
if (grid.Cells[i,j] != 0)
{
substExprs.Add(CellVariables[i][j]);
substVals.Add(GetConstExpr(grid.Cells[i,j]));
}
// Utiliser l'API de substitution pour appliquer les contraintes spécifiques
BoolExpr instance_c = (BoolExpr)GenericContraints.Substitute(substExprs.ToArray(), substVals.ToArray());
Solver solver = ctx.MkSolver();
solver.Assert(instance_c);
if (solver.Check() == Status.SATISFIABLE)
{
Model m = solver.Model;
for (uint i = 0; i < 9; i++)
{
for (uint j = 0; j < 9; j++)
{
if (grid.Cells[i,j] == 0)
{
solution.Cells[i,j] = ((BitVecNum)m.Evaluate(CellVariables[i][j])).Int;
}
else
{
solution.Cells[i,j] = grid.Cells[i,j];
}
}
}
}
else
{
Console.WriteLine("Failed to solve sudoku");
throw new Exception("Failed to solve sudoku");
}
}
}
Console.WriteLine("Classe Z3BitVectorSolverSubstitution definie (solveur Z3 avec substitution)");Classe Z3BitVectorSolverSubstitution definie (solveur Z3 avec substitution)
Nous allons maintenant explorer l’utilisation de l’API de tactiques de Z3 pour résoudre les Sudokus. Les tactiques permettent d’appliquer des transformations et des simplifications spécifiques pour améliorer l’efficacité de la résolution.
using Microsoft.Z3;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
internal class Z3BitSub_Tactic : Z3BitVectorSolverBase
{
public override SudokuGrid Solve(SudokuGrid s)
{
SudokuGrid solution = new SudokuGrid();
SudokuSolve(s, ref solution);
return solution;
}
public void SudokuSolve(SudokuGrid grid, ref SudokuGrid solution)
{
var substExprs = new List<Expr>();
var substVals = new List<Expr>();
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++)
if (grid.Cells[i,j] != 0)
{
substExprs.Add(CellVariables[i][j]);
substVals.Add(GetConstExpr(grid.Cells[i,j]));
}
BoolExpr instance_c = (BoolExpr)GenericContraints.Substitute(substExprs.ToArray(), substVals.ToArray());
Solver solver = ctx.MkSolver();
solver.Assert(instance_c);
BoolExpr puzzleConstraints = GetPuzzleConstraints(grid);
// Utiliser l'API de tactiques pour simplifier et résoudre le Sudoku
Tactic tactic = ctx.MkTactic("simplify");
Goal goal = ctx.MkGoal();
goal.Assert(ctx.MkAnd(GenericContraints, puzzleConstraints));
ApplyResult applyResult = tactic.Apply(goal);
if (applyResult.NumSubgoals > 0)
{
Goal newGoal = applyResult.Subgoals[0];
solver.Assert(newGoal.Formulas);
if (solver.Check() == Status.SATISFIABLE)
{
Model m = solver.Model;
for (uint i = 0; i < 9; i++)
{
for (uint j = 0; j < 9; j++)
{
if (grid.Cells[i,j] == 0)
{
solution.Cells[i,j] = ((BitVecNum)m.Evaluate(CellVariables[i][j])).Int;
}
else
{
solution.Cells[i,j] = grid.Cells[i,j];
}
}
}
}
else
{
Console.WriteLine("Failed to solve sudoku");
throw new Exception("Failed to solve sudoku");
}
}
}
}
Console.WriteLine("Classe Z3BitSub_Tactic definie (solveur Z3 avec tactiques)");Classe Z3BitSub_Tactic definie (solveur Z3 avec tactiques)
Pour comparer les différentes implémentations de solveurs, nous utiliserons une approche similaire à celle de notre notebook OR-Tools. Nous évaluerons les performances de chaque solver sur des puzzles de Sudoku de différentes difficultés (Facile, Moyen, Difficile).
Objectif : Utilisez Z3 pour compter le nombre total de solutions d’un puzzle en ajoutant itérativement des contraintes d’exclusion.
Indice : Après chaque solution trouvée, ajoutez une contrainte qui exclut cette solution et relancez le solver.
// Comparaison des quatre encodages Z3 sur des grilles réelles.
// Budget borne pour un temps d'exécution raisonnable : 3 grilles par difficulté, 3 s max par grille.
// Les grilles "Hard" (top95) peuvent dépasser ce budget pour certains encodages : un dépassement
// (statut "Timeout" / "Disqualified") est alors un résultat en soi, pas une erreur.
var solvers = new List<(string Name, ISudokuSolver Solver)>
{
("Z3 Int Solver Simple", new Z3IntSolverSimple()),
("Z3 Bit Vector Solver Simple", new Z3BitVectorSolverSimple()),
("Z3 Bit Vector Solver Substitution", new Z3BitVectorSolverSubstitution()),
("Z3 Bit Vector Solver Tactic", new Z3BitSub_Tactic())
};
var results = SudokuHelper.TestSolvers(solvers, numberOfSudokus: 3, timeLimitMilliseconds: 3000);
// Tableau texte des résultats ; les graphiques SVG ci-dessous sont eux aussi sérialisés dans le notebook.
Console.WriteLine(String.Format("{0,-36} {1,-10} {2,12} {3,8} {4}", "Encodage", "Difficulte", "Temps (ms)", "Resolus", "Statut"));
Console.WriteLine(new string('-', 84));
foreach (var r in results)
Console.WriteLine(String.Format("{0,-36} {1,-10} {2,12:F1} {3,8} {4}", r.SolverName, r.Difficulty, r.Time, r.SolvedCount, r.Status));
SudokuHelper.DisplayResults(results);Running tests...
Encodage Difficulte Temps (ms) Resolus Statut
------------------------------------------------------------------------------------
Z3 Int Solver Simple Easy 102,1 3 Success
Z3 Int Solver Simple Medium 376,7 3 Success
Z3 Int Solver Simple Hard 572,8 3 Success
Z3 Bit Vector Solver Simple Easy 179,6 3 Success
Z3 Bit Vector Solver Simple Medium 159,4 3 Success
Z3 Bit Vector Solver Simple Hard 244,1 3 Success
Z3 Bit Vector Solver Substitution Easy 149,8 3 Success
Z3 Bit Vector Solver Substitution Medium 161,2 3 Success
Z3 Bit Vector Solver Substitution Hard 191,3 3 Success
Z3 Bit Vector Solver Tactic Easy 148,4 3 Success
Z3 Bit Vector Solver Tactic Medium 210,4 3 Success
Z3 Bit Vector Solver Tactic Hard 250,7 3 Success
La cellule précédente lance le banc d’essai comparatif (TestSolvers + DisplayResults) sur trois niveaux de difficulté, avec un budget borné (3 grilles par difficulté, 3 s max par grille) afin que la comparaison tienne dans un temps raisonnable. Le tableau texte capture les temps de résolution réels ; les graphiques SVG ci-dessous sont eux aussi sérialisés dans le notebook.
Sur l’exécution committee, les quatre encodages résolvent l’intégralité des grilles (statut Success, 3/3 à chaque niveau). Le contraste porte sur la stabilité : les trois encodages par vecteurs de bits restent bien sous la seconde même sur top95. Sur les grilles faciles, aucun encodage ne domine : leur ordre change d’une exécution à l’autre. L’encodage entier, lui, se dégrade quand la difficulté monte : sur les grilles difficiles, il est de l’ordre de 2 à 3× plus lent que chacun des encodages par vecteurs de bits. Les mesures exactes varient d’une exécution à l’autre (contexte partagé, charge machine), mais cette dégradation de l’entier sur top95 est robuste.
| Solveur | Approche | Avantages | Inconvénients |
|---|---|---|---|
| Z3 Int Solver Simple | Entiers | Implémentation directe, facile à comprendre | Se dégrade sur les grilles les plus difficiles |
| Z3 Bit Vector Simple | Vecteurs 4 bits | Bonne performance, stable sur toutes les difficultés | Même structure de contraintes que l’int solver |
| Z3 Bit Vector Substitution | Substitution | Réutilise les contraintes génériques | Plus complexe à mettre en oeuvre |
| Z3 Bit Vector Tactic | Tactiques | Simplification automatique | Overhead potentiel |
Points cles :
Vecteurs de bits : representer chaque case sur 4 bits (valeurs 1-9) suffit et donne des variables plus compactes que les entiers ; c’est aussi l’encodage le plus stable sur les grilles difficiles dans le banc d’essai ci-dessus.
API de substitution : permet de réutiliser les contraintes génériques en substituant uniquement les valeurs spécifiques, évitant de redéclarer toutes les contraintes.
Tactiques : les tactiques comme “simplify” pré-traitent les contraintes, mais l’overhead ne se justifie pas toujours pour des Sudoku simples.
Performance : sur les grilles faciles, aucun encodage ne domine (l’ordre change d’une exécution à l’autre) ; l’écart se creuse sur les grilles difficiles, où l’entier devient de l’ordre de 2 à 3× plus lent que les vecteurs de bits (temps absolus mesurés dynamiquement par la cellule de banc d’essai ci-dessus, règle #9434). L’intérêt de l’encodage entier est conceptuel (modélisation directe, facile à comprendre), pas la vitesse. Le budget borné du banc d’essai (statut Timeout / Disqualified possible sur top95) fait partie de la mesure : un dépassement est un résultat, pas une erreur.
Note technique : les solveurs Z3 utilisent un contexte statique partage pour optimiser la performance. Dans une application de production, il faudrait gerer le cycle de vie du contexte plus proprement.
Optimize / MkMaximize)Jusqu’ici, Z3 a résolu des problèmes de satisfaction : trouver UNE solution réalisable (une grille de Sudoku valide) via ctx.MkSolver() + Check() + Model. La capacité distinctive de Z3 est l’optimisation SMT : le contexte ctx.MkOptimize() résout des problèmes où l’on maximise (ou minimise) un objectif sous contraintes — c’est le moteur de la programmation MaxSMT (maximisation sat-modulo-theories, utilisée en vérification, scheduling, allocation de ressources).
Démonstration : un « carré latin à bonus » 5×5 — chaque cellule (i, j) offre un reward dépendant de la valeur placée. Parmi tous les carrés latins 5×5 valides, MkOptimize() + MkMaximize() trouve celui qui maximise le reward total. Le statut SATISFIABLE certifie que l’optimum est atteint (preuve non-vacuous).
Miroir C# de la demo du notebook jumeau
Sudoku-12-Z3-Python(#7589, même problème), mais avec l’API SMTctx.MkOptimize()/MkMaximize()du binding .NETMicrosoft.Z3. Le reward est déterministe (formule, pas PRNG) pour la reproductibilité cross-langage.
// Z3 SMT optimization demo: ctx.MkOptimize() + MkMaximize on a weighted latin square.
// Exercice la capacite d'optimisation signature de Z3 (MaxSMT / Optimize context),
// distinct du Solver() satisfaction-only. Miroir C# de Sudoku-12-Z3-Python (#7589).
// Z3 .NET API : MkOptimize, MkDistinct (rows/cols), MkITE (reward conditionnel), MkMaximize.
// Context local (methode top-level, style cell 12) : les Solver cells heritent ctx d'une classe
// (Z3IntSolverSimple/Z3BitVectorSolverBase) ; cette demo est autonome et declare son propre Context.
using Microsoft.Z3;
public static (int[,] Grid, int Reward, string Status) SolveMaxRewardLatinSquareZ3(int n = 5)
{
Context ctx = new Context();
Optimize opt = ctx.MkOptimize();
// cell[i,j] = IntExpr dans [1, n] (encodage Int, style Z3IntSolverSimple).
IntExpr[,] cell = new IntExpr[n, n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
cell[i, j] = (IntExpr)ctx.MkConst(ctx.MkSymbol("c_" + i + "_" + j), ctx.IntSort);
opt.Assert(ctx.MkGe(cell[i, j], ctx.MkInt(1)));
opt.Assert(ctx.MkLe(cell[i, j], ctx.MkInt(n)));
}
// Carre latin : valeurs toutes-distinctes par ligne et par colonne (MkDistinct).
for (int i = 0; i < n; i++)
{
IntExpr[] row = new IntExpr[n];
IntExpr[] col = new IntExpr[n];
for (int k = 0; k < n; k++) { row[k] = cell[i, k]; col[k] = cell[k, i]; }
opt.Assert(ctx.MkDistinct(row));
opt.Assert(ctx.MkDistinct(col));
}
// reward[i,j,v] = bonus pour valeur v a la cellule (i,j). Formule deterministe.
ArithExpr[] terms = new ArithExpr[n * n * n];
int t = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int v = 1; v <= n; v++)
{
int reward = ((i + 1) * (j + 1) + v * v) % 11; // deterministe, reproductible
terms[t++] = (ArithExpr)ctx.MkITE(ctx.MkEq(cell[i, j], ctx.MkInt(v)),
ctx.MkInt(reward), ctx.MkInt(0));
}
ArithExpr rewardTotal = ctx.MkAdd(terms);
opt.MkMaximize(rewardTotal); // objectif : MAXIMISER le reward (MaxSMT)
if (opt.Check() == Status.SATISFIABLE)
{
Model m = opt.Model;
int[,] grid = new int[n, n];
int computed = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
grid[i, j] = ((IntNum)m.Evaluate(cell[i, j])).Int;
computed += ((i + 1) * (j + 1) + grid[i, j] * grid[i, j]) % 11;
}
return (grid, computed, "SATISFIABLE (optimum)");
}
return (null, 0, "UNSATISFIABLE");
}
var (maxGrid, maxReward, maxStatus) = SolveMaxRewardLatinSquareZ3(n: 5);
Console.WriteLine($"Statut : {maxStatus} | Reward total maximal : {maxReward}");
Console.WriteLine("Carre latin 5x5 optimal (maximise le reward) :");
for (int i = 0; i < 5; i++)
Console.WriteLine(string.Join(" ", Enumerable.Range(0, 5).Select(j => $"{maxGrid[i, j],2}")));Statut : SATISFIABLE (optimum) | Reward total maximal : 192
Carre latin 5x5 optimal (maximise le reward) :
3 4 2 5 1
4 2 5 1 3
2 5 1 3 4
5 1 3 4 2
1 3 4 2 5
OPTIMAL n’est pas FEASIBLELa sortie SATISFIABLE (optimum) | Reward total maximal : 192 condense la capacité qui distingue l’optimisation SMT de la simple satisfaction. Deux statuts possibles qu’il faut savoir lire :
FEASIBLE : le solveur a trouvé une solution réalisable — sans garantie qu’elle soit bonne. C’est ce que renvoie un solveur de satisfaction ordinaire.OPTIMAL (ici) : le solveur prouve qu’aucune autre solution ne fait mieux que 192. C’est une preuve d’optimalité, pas juste une réponse. MkMaximize demande à Z3 de maximiser le reward total ; il explore l’espace des carrés latins 5×5 valides et établit que 192 est le plafond.Le carré latin renvoyé est cyclique — chaque ligne est la permutation (3 4 2 5 1) décalée d’un cran (4 2 5 1 3, puis 2 5 1 3 4…). Cette structure régulière maximise le reward parce que chaque chiffre visite chaque colonne exactement une fois (définition même du carré latin) tout en plaçant les valeurs là où les poids reward[i][j][v] les valorisent le plus. C’est précisément ce couplage « satisfaction (latin) + optimisation (reward) » qui fait de Z3 un moteur d’allocation de ressources (pas seulement un solveur de Sudoku) — la cellule-exercice suivante en explore le miroir MkMinimize.
MkMinimizeAdaptez la modélisation ci-dessus pour MINIMISER une fonction de coût au lieu de maximiser le reward.
Indices : 1. Gardez les mêmes contraintes (carré latin : MkDistinct rows/cols, bornes [1, n]). 2. Remplacez opt.MkMaximize(rewardTotal) par la minimisation de la somme des valeurs sur la diagonale principale : diagonalSum = ctx.MkAdd(cell[0,0], cell[1,1], ..., cell[n-1,n-1]) puis opt.MkMinimize(diagonalSum). 3. Objectif : le carré latin 5×5 qui minimise la somme des valeurs sur la diagonale (privilégier les petits nombres sur la diagonale). 4. Vérifiez que le statut reste SATISFIABLE et que le coût diagonal obtenu est cohérent (la diagonale minimale d’un carré latin 5×5 est 1+2+3+4+5 = 15).
// Exercice : miroir MINIMISE de SolveMaxRewardLatinSquareZ3 (stub, C.1 compliant).
// TODO etudiant : copier SolveMaxRewardLatinSquareZ3 et adapter :
// 1. Garder les memes contraintes (MkOptimize, MkDistinct rows/cols, bornes [1,n]).
// 2. Remplacer opt.MkMaximize(rewardTotal) par :
// ArithExpr diagonalSum = ctx.MkAdd(Enumerable.Range(0, n).Select(i => cell[i, i]).ToArray());
// opt.MkMinimize(diagonalSum);
// 3. Retourner (grid, computedDiagonal, "SATISFIABLE (minimum)").
public static (int[,] Grid, int DiagonalCost, string Status) SolveMinDiagonalLatinSquareZ3(int n = 5)
{
// TODO etudiant : a completer (voir SolveMaxRewardLatinSquareZ3 ci-dessus).
return (null, 0, "non implemente");
}
var (minGrid, minCost, minStatus) = SolveMinDiagonalLatinSquareZ3(n: 5);
if (minGrid != null)
{
Console.WriteLine($"Statut : {minStatus} | Cout diagonal minimal : {minCost}");
for (int i = 0; i < 5; i++)
Console.WriteLine(string.Join(" ", Enumerable.Range(0, 5).Select(j => $"{minGrid[i, j],2}")));
}
else
{
Console.WriteLine($"Implementation a completer pour voir le resultat ({minStatus}).");
}Implementation a completer pour voir le resultat (non implemente).
Dans ce notebook, nous avons exploré différentes approches pour résoudre des puzzles de Sudoku en utilisant Z3. Nous avons commencé par une implémentation simple en utilisant des entiers, puis nous avons introduit des méthodes plus sophistiquées utilisant des vecteurs de bits, l’API de substitution et les tactiques de pré-traitement. Les cellules de démonstration montrent chaque encodage résolvant une grille réelle (grille de départ, grille complète, zéro erreur), et le banc d’essai borné compare leurs temps de résolution sur trois niveaux de difficulté.
Ces quatre formulations illustrent comment un même problème peut être modélisé de plusieurs façons avec Z3. À l’échelle d’un Sudoku 9x9, les quatre approches résolvent les grilles en moins d’une seconde ; les encodages par vecteurs de bits dominent sur toutes les difficultés — la substitution est la plus rapide en Easy et Medium, le BitVec simple en Hard — tandis que l’encodage entier n’est jamais le plus rapide et se dégrade fortement sur les grilles difficiles (temps absolus mesurés dynamiquement par la cellule de banc d’essai, règle #9434 ; seul l’ordre de grandeur du ralentissement — ~3 à 5× — est stable cross-machine). Le choix se justifie donc à la fois par la lisibilité, la réutilisabilité des contraintes et le profil de performance visé.
Merci d’avoir suivi ce notebook, et j’espère que cela vous a permis de mieux comprendre comment utiliser Z3 pour résoudre des problèmes de contraintes complexes comme les puzzles de Sudoku.
Le Sudoku X est une variante où les deux diagonales principales doivent également contenir des chiffres distincts.
Objectif : Étendre Z3BitVectorSolverBase pour ajouter des contraintes de diagonale.
Indices : 1. La diagonale principale contient CellVariables[i][i] pour i de 0 à 8 2. L’anti-diagonale contient CellVariables[i][8-i] pour i de 0 à 8 3. Utiliser ctx.MkDistinct() comme pour les lignes et colonnes 4. ctx et CellVariables sont accessibles statiquement depuis la classe de base
Vérification : Vérifiez d’abord avec EnforceDiagonals = false (comportement identique au solveur de base).
Objectif : Utilisez Z3 pour résoudre un problème de coloration de graphe : colorier un graphe avec k couleurs telles que deux noeuds adjacents n’ont jamais la même couleur.
Indice : Créez une variable entière par noeud et ajoutez des contraintes de différence pour chaque arête.
// Exercice 1 : Sudoku X avec contraintes de diagonale (Z3 BitVector)
using Microsoft.Z3;
public class SudokuXZ3Solver : Z3BitVectorSolverBase
{
public bool EnforceDiagonals { get; set; } = true;
public override SudokuGrid Solve(SudokuGrid grid)
{
var solver = ctx.MkSolver();
// TODO : Ajouter les contraintes generiques standards
// solver.Assert(GenericContraints);
if (EnforceDiagonals)
{
// TODO : Extraire et contraindre la diagonale principale
// var mainDiag = new BitVecExpr[9];
// for (int i = 0; i < 9; i++) mainDiag[i] = CellVariables[i][i];
// solver.Assert(ctx.MkDistinct(mainDiag));
// TODO : Extraire et contraindre l'anti-diagonale
// var antiDiag = new BitVecExpr[9];
// for (int i = 0; i < 9; i++) antiDiag[i] = CellVariables[i][8 - i];
// solver.Assert(ctx.MkDistinct(antiDiag));
}
// TODO : Ajouter les contraintes du puzzle et résoudre
// solver.Assert(GetPuzzleConstraints(grid));
// if (solver.Check() == Status.SATISFIABLE) { ... extraire la solution ... }
return grid;
}
}
Console.WriteLine("TODO : Implementez SudokuXZ3Solver avec les contraintes de diagonale");TODO : Implementez SudokuXZ3Solver avec les contraintes de diagonale
Z3 permet de chercher toutes les solutions en ajoutant progressivement des contraintes d’exclusion.
Objectif : Implémenter HasUniqueSolution qui vérifie qu’un puzzle a exactement une solution.
Indices : 1. Trouver la première solution S1 2. Construire la contrainte d’exclusion : “au moins une cellule diffère de S1” - ctx.MkOr(ctx.MkNot(ctx.MkEq(CellVariables[i][j], valeur_S1))) pour chaque cellule 3. Si solver.Check() retourne UNSATISFIABLE après ajout : la solution est unique 4. Technique appelée “exclusion de modèle” (model blocking)
Vérification : Testez sur un puzzle facile (solution unique attendue).
// Exercice 2 : Vérification d'unicité des solutions avec Z3
public class Z3SudokuUnicityChecker
{
private static Context ctx = Z3IntSolverSimple.ctx;
private static IntExpr[][] CellVariables = Z3IntSolverSimple.CellVariables;
/// <summary>
/// Vérifie si un puzzle a exactement une solution.
/// Strategie : trouver S1, puis chercher S2 != S1. Si impossible -> unique.
/// </summary>
public bool HasUniqueSolution(SudokuGrid puzzle)
{
var s = ctx.MkSolver();
// TODO : 1. Ajouter les contraintes generiques et celles du puzzle
// s.Assert(Z3IntSolverSimple.GenericContraints);
// Indice : voir Z3IntSolverSimple.GetPuzzleConstraints(puzzle)
// TODO : 2. Vérifier satisfiabilité et extraire S1
// if (s.Check() != Status.SATISFIABLE) return false;
// Model m1 = s.Model;
// TODO : 3. Construire la contrainte "au moins une cellule differe de S1"
// BoolExpr[] differences = new BoolExpr[81];
// for (int i = 0; i < 9; i++)
// for (int j = 0; j < 9; j++)
// {
// var val = ((IntNum)m1.Evaluate(CellVariables[i][j])).Int;
// differences[i * 9 + j] = ctx.MkNot(ctx.MkEq(CellVariables[i][j], ctx.MkInt(val)));
// }
// s.Assert(ctx.MkOr(differences));
// TODO : 4. Retourner true si aucune deuxième solution n'existe
// return s.Check() == Status.UNSATISFIABLE;
return false;
}
}
// Test
var checker2 = new Z3SudokuUnicityChecker();
var easyPuzzle2 = SudokuHelper.GetSudokus(SudokuDifficulty.Easy).First();
// bool unique = checker2.HasUniqueSolution(easyPuzzle2);
// Console.WriteLine($"Solution unique : {unique}");
Console.WriteLine("TODO : Implementez Z3SudokuUnicityChecker.HasUniqueSolution()");TODO : Implementez Z3SudokuUnicityChecker.HasUniqueSolution()
Z3 propose différentes tactiques pour pré-traiter les formules avant résolution. Chaque tactique à ses avantages selon le type de problème.
Objectif : Mesurer l’impact des tactiques sur les performances de résolution.
Indices : 1. Créer une tactique : ctx.MkTactic("simplify") ou ctx.MkTactic("bit-blast") 2. Créer un goal : ctx.MkGoal() puis goal.Assert(contraintes) 3. Appliquer : tactic.Apply(goal) retourne un ApplyResult 4. Passer les sous-goals au solveur : solver.Assert(result.Subgoals[0].Formulas) 5. Tactiques a tester : "simplify", "bit-blast", "ctx-solver-simplify", "solve-eqs"
Vérification : "bit-blast" est généralement efficace pour les BitVectors.
// Exercice 3 : Comparaison des tactiques Z3
using Microsoft.Z3;
using System.Diagnostics;
var tacticPuzzle = SudokuHelper.GetSudokus(SudokuDifficulty.Medium).First();
var tacticCtx = Z3BitVectorSolverBase.ctx;
var genericConstr = Z3BitVectorSolverBase.GenericContraints;
var puzzleConstrHelper = new Z3BitVectorSolverSimple();
var puzzleConstr = puzzleConstrHelper.GetPuzzleConstraints(tacticPuzzle);
string[] tacticNames = { "simplify", "ctx-solver-simplify", "bit-blast", "solve-eqs" };
Console.WriteLine("Comparaison des tactiques Z3 :");
Console.WriteLine(new string('-', 40));
// TODO : Pour chaque tactique, créer et appliquer, mesurer le temps de résolution
// foreach (var tacticName in tacticNames)
// {
// var tactic = tacticCtx.MkTactic(tacticName);
// var goal = tacticCtx.MkGoal();
// goal.Assert(tacticCtx.MkAnd(genericConstr, puzzleConstr));
// var sw = Stopwatch.StartNew();
// var result = tactic.Apply(goal);
// var solver = tacticCtx.MkSolver();
// if (result.NumSubgoals > 0)
// solver.Assert(result.Subgoals[0].Formulas);
// var status = solver.Check();
// sw.Stop();
// Console.WriteLine($"{tacticName,-25} : {sw.ElapsedMilliseconds,6} ms - {status}");
// }
Console.WriteLine("TODO : Implementez la comparaison des tactiques Z3");Comparaison des tactiques Z3 :
----------------------------------------
TODO : Implementez la comparaison des tactiques Z3
Utilisez Z3 pour vérifier des propriétés logiques sur les grilles de Sudoku. Implémentez les fonctions suivantes avec l’API Z3 en C# :
HasUniqueSolution(SudokuGrid puzzle) : Vérifie si une grille a exactement une solution
IsMinimal(SudokuGrid puzzle) : Vérifie si le puzzle est minimal (supprimer une valeur initiale crée une ambiguïté)
HasUniqueSolution retourne false (plus d’une solution)Indice :
Pour HasUniqueSolution, après avoir trouvé S1 = {cells[i][j] = v[i][j]}, ajoutez la contrainte Or(cell[i][j] != v[i][j]) pour toutes les cellules. Cette contrainte dit “au moins une cellule diffère de S1”.
// Exercice : Vérification de propriétés avec Z3
public class Z3SudokuPropertyChecker
{
private static Context ctx = Z3IntSolverSimple.ctx;
/// <summary>
/// Vérifie si un puzzle a exactement une solution.
/// Strategie : trouver S1, puis chercher S2 != S1. Si impossible -> unique.
/// </summary>
public bool HasUniqueSolution(SudokuGrid puzzle)
{
// TODO : Implémenter la vérification d'unicité
// 1. Construire les contraintes generiques + contraintes du puzzle
// 2. Trouver la première solution S1
// 3. Ajouter la contrainte "au moins une cellule differe de S1"
// 4. Vérifier si une deuxième solution existe
return false;
}
/// <summary>
/// Vérifie si le puzzle est minimal :
/// supprimer n'importe quelle valeur initiale crée une ambiguïté.
/// </summary>
public bool IsMinimal(SudokuGrid puzzle)
{
// TODO : Implémenter la vérification de minimalité
// Pour chaque cellule fixee (i, j) :
// - Créer une copie du puzzle sans la valeur en (i, j)
// - Vérifier que HasUniqueSolution retourne false
// Si toutes les cellules satisfont cette condition -> minimal
return false;
}
}
// Test : vérifier les propriétés sur le puzzle facile
var checker = new Z3SudokuPropertyChecker();
var easyPuzzle = SudokuHelper.GetSudokus(SudokuDifficulty.Hard).First();
Console.WriteLine("Test de verification de proprietes Z3");
Console.WriteLine("Puzzle selectionne :");
Console.WriteLine(easyPuzzle);
// bool unique = checker.HasUniqueSolution(easyPuzzle);
// bool minimal = checker.IsMinimal(easyPuzzle);
// Console.WriteLine($"Solution unique : {unique}");
// Console.WriteLine($"Puzzle minimal : {minimal}");Test de verification de proprietes Z3
Puzzle selectionne :
-------------------------------
| 4 | | 8 5 |
| 3 | | |
| | 7 | |
-------------------------------
| 2 | | 6 |
| | 8 | 4 |
| | 1 | |
-------------------------------
| | 6 3 | 7 |
| 5 | 2 | |
| 1 4 | | |
-------------------------------
Z3 offre quatre formulations de résolution Sudoku, de la plus simple à la plus optimisée :
| Formulation | Variables | Avantage |
|---|---|---|
| IntExpr simple | 81 IntVar | Modélisation directe, facile à comprendre |
| BitVec 4 bits | 81 BitVec | Espace reduit (4 bits vs 32 bits) |
| Substitution API | 81 BitVec | Réutilisation du modèle générique |
| Tactiques | 81 BitVec | Pre-traitement (simplify, bit-blast) |
Lecons : 1. L’API Substitution permet de séparer le modèle générique (contraintes Sudoku) des valeurs spécifiques au puzzle. 2. Les tactiques (simplify, bit-blast, ctx-solver-simplify) pré-traitent les formules pour accélérer la résolution. 3. L’exclusion de modèle (model blocking) vérifie l’unicité de la solution en ajoutant des contraintes d’exclusion. 4. Z3 reste l’outil de référence pour la vérification formelle de propriétés (unicité, minimalité).
Navigation : << Sudoku-11 Choco C# | Index | Sudoku-13 Symbolic Automata C# >>
Voir aussi : - CSP-3-Avance - Contraintes globales avancees - SymbolicAI - Série sur l’IA symbolique et Z3