Problèmes de Satisfaction de Contraintes - Fondamentaux (.NET)
Ce notebook est le binome .NET du notebook Python CSP-1-Fundamentals.ipynb. Nous y implementons les mêmes algorithmes en C#, puis utilisons Choco-solver 4.10.17 (via IKVM 8.15.0) comme solveur SOTA pour comparer les performances et la concision du code.
Objectifs d apprentissage
A la fin de ce notebook, vous saurez : 1. Formaliser un problème sous forme de CSP en C# (variables, domaines, contraintes) (Bloom : comprendre) 2. Implementer l algorithme de backtracking pour CSP en C# (Bloom : appliquer) 3. Appliquer les heuristiques MRV et LCV pour accelerer la recherche (Bloom : analyser) 4. Modeliser des problemes classiques avec Choco-solver (API Java via IKVM) (Bloom : appliquer) 5. Comparer les approches manuelles C# et la bibliothèque Choco sur des problemes concrets (Bloom : évaluer)
Prérequis
Bases de C# (classes, dictionnaires, recursivite)
Notions de recherche dans un espace d états (Search-1)
Familiarite avec les CSP (notebook Python conseille mais pas obligatoire)
La DLL Choco est déployée dans le dossier Part2-CSP (12 MB). Le bloc #r "nuget:..." charge IKVM en mémoire, puis #r "org.chocosolver.solver.dll" charge Choco. La JVM sous-jacente démarre au premier appel de type java.*.
// Localisation du dossier Part2-CSP et de la DLL Choco pré-compiléeusing System;using System.IO;stringFindCspDir(){var dir =newDirectoryInfo(Directory.GetCurrentDirectory());while(dir !=null){if(File.Exists(Path.Combine(dir.FullName,"CSP-1-Fundamentals.ipynb")))return dir.FullName; dir = dir.Parent;}return Directory.GetCurrentDirectory();}var cspDir =FindCspDir();var dllPath = Path.Combine(cspDir,"org.chocosolver.solver.dll");var dllInfo =newFileInfo(dllPath);Console.WriteLine($"DLL Choco trouvee : {dllInfo.Name}");Console.WriteLine($"Taille : {dllInfo.Length / 1024.0 / 1024.0:F1} MB");Console.WriteLine($"Date de build : {dllInfo.LastWriteTime:yyyy-MM-dd}");if(!dllInfo.Exists){thrownewFileNotFoundException("DLL Choco introuvable", dllPath);}
The below script needs to be able to find the current output cell; this is an easy method to get it.
DLL Choco trouvee : org.chocosolver.solver.dll
Taille : 11.4 MB
Date de build : 2026-09-24
// Configuration IKVM 8.15.0 pour Choco-solver -- recette #4711 (IkvmCopyMerge récursif)// Leve "Could not locate ikvm home path" : IKVM 8.15 lit AppContext["IKVM.Home"], pas la variable// d'environnement. On assemble le home complet (fusion image arch-indépendante any/any + native// de la plateforme) via copie RÉCURSIVE (la copie plate rate les sous-dossiers lib/ et tzdb.dat), AVANT// tout premier appel Java (l'init JVM se declenche au premier type java.*, cellule suivante).// See #4667, See #3801, See #4956.#r "nuget: IKVM, 8.15.0"#r "nuget: IKVM.Image, 8.15.0"using System.IO;using System.Runtime.InteropServices;// RID de la machine courante (win-x64, linux-x64, osx-arm64, ...) : le paquet IKVM.Image// tire deja l'image native de chaque plateforme, il suffit de choisir la bonne.string ikvmOs = OperatingSystem.IsWindows()?"win": OperatingSystem.IsMacOS()?"osx":"linux";string ikvmRid = ikvmOs +"-"+ RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();string ikvmVer ="8.15.0";string nugetRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES")?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),".nuget","packages");string ikvmBaseAny = Path.Combine(nugetRoot,"ikvm.image", ikvmVer,"ikvm","any","any");string ikvmArchDir = Path.Combine(nugetRoot,"ikvm.image.runtime."+ ikvmRid, ikvmVer,"ikvm","any", ikvmRid);string ikvmHome = Path.Combine(Path.GetTempPath(),"ikvm-home-"+ ikvmVer +"-"+ ikvmRid);voidIkvmCopyMerge(string src,string dst){foreach(var d in Directory.GetDirectories(src,"*", SearchOption.AllDirectories)) Directory.CreateDirectory(d.Replace(src, dst));foreach(var f in Directory.GetFiles(src,"*", SearchOption.AllDirectories)){var t = f.Replace(src, dst); Directory.CreateDirectory(Path.GetDirectoryName(t)); File.Copy(f, t, overwrite:true);}}if(Directory.Exists(ikvmBaseAny)&& Directory.Exists(ikvmArchDir)){ Directory.CreateDirectory(ikvmHome);IkvmCopyMerge(ikvmBaseAny, ikvmHome);IkvmCopyMerge(ikvmArchDir, ikvmHome);}AppContext.SetData("IKVM.Home", ikvmHome);bool tzdbOk = File.Exists(Path.Combine(ikvmHome,"lib","tzdb.dat"));Console.WriteLine("IKVM 8.15.0 pret (home="+ Path.GetFileName(ikvmHome)+", tzdb="+ tzdbOk +") - Choco-solver charge");
Installed Packages
IKVM, 8.15.0
IKVM.Image, 8.15.0
IKVM 8.15.0 pret (home=ikvm-home-8.15.0-linux-x64, tzdb=True) - Choco-solver charge
// Chargement de la DLL Choco-solver 4.10.17 et import des namespaces principaux#r "org.chocosolver.solver.dll"using org.chocosolver.solver;using org.chocosolver.solver.variables;using org.chocosolver.solver.constraints;using System.Collections.Generic;Console.WriteLine("Choco-solver 4.10.17 charge -- Model, IntVar, arithm, allDifferent disponibles");
Un CSP (Constraint Satisfaction Problem) est un triplet \((X, D, C)\) :
\(X = \{X_1, X_2, \ldots, X_n\}\) : ensemble de variables
\(D = \{D_1, D_2, \ldots, D_n\}\) : ensemble de domaines, ou \(D_i\) est l ensemble des valeurs possibles pour \(X_i\)
\(C = \{C_1, C_2, \ldots, C_m\}\) : ensemble de contraintes, chacune portant sur un sous-ensemble de variables
Types de contraintes
Type
Porte sur
Exemple Choco
Unaire
1 variable
model.arithm(v, "!=", 5).post()
Binaire
2 variables
model.arithm(v1, "!=", v2).post()
Globale
\(k > 2\) variables
model.allDifferent(vars).post()
Graphe de contraintes
On représente un CSP binaire par un graphe de contraintes ou chaque noeud est une variable et chaque arête relie deux variables partageant une contrainte.
Implémentons une classe CSP générique en C# (équivalent direct de la classe Python du notebook source). Elle servira pour les sections 3 et 4 (implémentation manuelle), avant de basculer sur Choco en section 5.
// Classe CSP générique en C# (binaire)// Variables, domaines, voisins, et une fonction de contrainte utilisateur.using System.Collections.Generic;publicclass CSP {public List<string> Variables {get;}public Dictionary<string, List<object>> Domains {get;}public Dictionary<string, List<string>> Neighbors {get;}public Func<string,object,string,object,bool> ConstraintFunc {get;}publicint NAssigns {get;privateset;}publicCSP(List<string> variables, Dictionary<string, List<object>> domains, Dictionary<string, List<string>> neighbors, Func<string,object,string,object,bool> constraintFunc){ Variables = variables; Domains =new Dictionary<string, List<object>>();foreach(var kv in domains) Domains[kv.Key]=new List<object>(kv.Value); Neighbors = neighbors; ConstraintFunc = constraintFunc; NAssigns =0;}publicboolConsistent(string var,object val, Dictionary<string,object> assignment){if(!Neighbors.TryGetValue(var,outvar neigh))returntrue;foreach(var otherVar in neigh){if(assignment.TryGetValue(otherVar,outvar otherVal)){if(!ConstraintFunc(var, val, otherVar, otherVal))returnfalse;}}returntrue;}publicboolIsComplete(Dictionary<string,object> assignment)=> assignment.Count== Variables.Count;publicboolIsSolution(Dictionary<string,object> assignment){if(!IsComplete(assignment))returnfalse;foreach(var v in Variables){if(!Consistent(v, assignment[v], assignment))returnfalse;}returntrue;}publicvoidResetCounter()=> NAssigns =0;}Console.WriteLine("Classe CSP C# définie.");
Classe CSP C# définie.
3. Exemple : coloration de carte de l’Australie (~8 min)
Le problème classique de coloration de carte consiste à colorier les régions de sorte que deux régions adjacentes n aient jamais la même couleur. C est l exemple canonique du livre AIMA (Russell & Norvig), appliqué à la carte de l’Australie avec ses 7 états/territoires.
Modelisation CSP
Composant
Valeur
Variables
WA, NT, SA, Q, NSW, V, T (les 7 états)
Domaines
{Rouge, Vert, Bleu} pour chaque variable
Contraintes
Regions adjacentes \(\neq\) même couleur
Sans contraintes, \(3^7 = 2187\) combinaisons sont envisageables. Les contraintes eliminent la majorite.
// Coloration de l'Australie : variables, domaines, voisins, contrainte binairevar australiaVars =new List<string>{"WA","NT","SA","Q","NSW","V","T"};var australiaDomains =new Dictionary<string, List<object>>();foreach(var v in australiaVars) australiaDomains[v]=new List<object>{"Rouge","Vert","Bleu"};var australiaNeighbors =new Dictionary<string, List<string>>{{"WA",new List<string>{"NT","SA"}},{"NT",new List<string>{"WA","SA","Q"}},{"SA",new List<string>{"WA","NT","Q","NSW","V"}},{"Q",new List<string>{"NT","SA","NSW"}},{"NSW",new List<string>{"Q","SA","V"}},{"V",new List<string>{"SA","NSW"}},{"T",new List<string>()}// Tasmanie isolee};Func<string,object,string,object,bool> differentColors =(v1, val1, v2, val2)=>!val1.Equals(val2);var australiaCSP =newCSP(australiaVars, australiaDomains, australiaNeighbors, differentColors);Console.WriteLine("Problème de coloration de l Australie");Console.WriteLine("=============================================");Console.WriteLine($"Variables : {string.Join(",", australiaVars)}");Console.WriteLine($"Taille dom. : {australiaDomains["WA"].Count} couleurs");Console.WriteLine($"Espace brut : {Math.Pow(3, 7)} combinaisons");Console.WriteLine();Console.WriteLine("Adjacences :");foreach(var v in australiaVars){ Console.WriteLine($" {v,3} -> [{string.Join(",", australiaNeighbors[v])}]");}
Problème de coloration de l Australie
=============================================
Variables : WA, NT, SA, Q, NSW, V, T
Taille dom. : 3 couleurs
Espace brut : 2187 combinaisons
Adjacences :
WA -> [NT, SA]
NT -> [WA, SA, Q]
SA -> [WA, NT, Q, NSW, V]
Q -> [NT, SA, NSW]
NSW -> [Q, SA, V]
V -> [SA, NSW]
T -> []
Lecture de l’instance (cellule ci-dessus) :
L’instance affichée fixe le cadre de toute la suite du notebook :
Espace brut = 2187 combinaisons : 3 couleurs élevées à 7 variables (3⁷). C’est ce chiffre que la recherche exhaustive de la section suivante balaiera intégralement — et la raison pour laquelle une instance aussi petite reste démonstrative.
SA est le hub du graphe : 5 voisins (WA, NT, Q, NSW, V) contre 2 à 3 pour les autres — c’est lui que l’heuristique MRV de la section 5 sélectionnera en premier.
La Tasmanie (T) est isolée : aucune adjacence, donc aucune contrainte — elle colorie librement et forme un sous-problème indépendant.
Le graphe de contraintes compte 9 arêtes (chaque adjacence comptée une fois) — c’est exactement le graphe visualisé et interprété dans les deux cellules suivantes.
// Visualisation ASCII du graphe de contraintes// (les notebooks C# du projet n utilisent pas matplotlib -- le CSP-7-Soft-CSharp// montre que les viz se font en matrice couleur ou en texte structure)// Orientation AIMA-compatible : NT au nord, SA au centre (hub degré 5),// WA à l ouest, Q au nord-est, NSW à l est, V au sud-est (sous NSW),// T isolee au sud (ile de Tasmanie). Cette orientation reflete la geographic// réelle et la position relative des États (cf. notebook Python jumeau).Console.WriteLine("Graphe de contraintes - Australie (vue texte)");Console.WriteLine("=============================================");Console.WriteLine();Console.WriteLine(@" NT (deg 3 - Territoire du Nord)");Console.WriteLine(@" /|\");Console.WriteLine(@" / | \");Console.WriteLine(@" / | \");Console.WriteLine(@" / | \");Console.WriteLine(@" WA | Q (deg 3 - Queensland)");Console.WriteLine(@" \ | /");Console.WriteLine(@" \ | /");Console.WriteLine(@" \ | /");Console.WriteLine(@" SA (deg 5 - noeud le plus contraint, hub)");Console.WriteLine(@" /|\");Console.WriteLine(@" / | \");Console.WriteLine(@" / | \");Console.WriteLine(@" / | \");Console.WriteLine(@" NSW | V (deg 2 - Victoria, au sud de NSW)");Console.WriteLine(@" |");Console.WriteLine(@" T (deg 0 - Tasmanie, ile isolee au sud)");Console.WriteLine();Console.WriteLine("Arêtes : 9 paires de contraintes binaires (régions adjacentes != même couleur)");
Graphe de contraintes - Australie (vue texte)
=============================================
NT (deg 3 - Territoire du Nord)
/|\
/ | \
/ | \
/ | \
WA | Q (deg 3 - Queensland)
\ | /
\ | /
\ | /
SA (deg 5 - noeud le plus contraint, hub)
/|\
/ | \
/ | \
/ | \
NSW | V (deg 2 - Victoria, au sud de NSW)
|
T (deg 0 - Tasmanie, ile isolee au sud)
Arêtes : 9 paires de contraintes binaires (régions adjacentes != même couleur)
Interprétation : le graphe de contraintes de l’Australie a 7 noeuds et 9 arêtes. SA (Australie-Méridionale) est le noeud le plus contraint (5 voisins) – c est lui qui elimine le plus de combinaisons. T (Tasmanie) est isolee : n importe quelle couleur lui convient.
Points clés : 1. SA est adjacent à presque tous les états continentaux : c est la variable la plus contrainte 2. T (Tasmanie) n a aucune contrainte : n importe quelle couleur convient 3. Le graphe n est pas complet : seules les paires adjacentes sont contraintes
4. Backtracking Search (~10 min)
Principe
Le backtracking est l algorithme de base pour résoudre un CSP :
Choisir une variable non encore assignee
Pour chaque valeur de son domaine :
Vérifier la consistance avec l assignation partielle
Si consistant, assigner et recurser sur les variables restantes
Sinon, essayer la valeur suivante
Si aucune valeur n est valable, revenir en arrière (backtrack)
Différence avec DFS classique
Aspect
DFS classique
Backtracking CSP
Assignation
Complète d abord, vérification ensuite
Vérification à chaque étape
Elagage
Aucun
Elimination des branches inconsistantes
// Brute force : enumerer toutes les combinaisons et filtrer// Sert de référence pour mesurer l'apport du backtrackingusing System.Diagnostics;var colors =new List<object>{"Rouge","Vert","Bleu"};var allCombos =new List<Dictionary<string,object>>();// produit cartesien des 7 domainesvoidGenerateCombos(int idx, Dictionary<string,object> current){if(idx == australiaVars.Count){ allCombos.Add(new Dictionary<string,object>(current));return;}var v = australiaVars[idx];foreach(var val in australiaDomains[v]){ current[v]= val;GenerateCombos(idx +1, current);}}GenerateCombos(0,new Dictionary<string,object>());var swBF = Stopwatch.StartNew();int nTotal =0, nValid =0;foreach(var assignment in allCombos){ nTotal++;if(australiaCSP.IsSolution(assignment)) nValid++;}swBF.Stop();Console.WriteLine("Brute force - Coloration Australie");Console.WriteLine("=============================================");Console.WriteLine($"Combinaisons testees : {nTotal}");Console.WriteLine($"Solutions trouvees : {nValid}");Console.WriteLine($"Ratio solutions : {(double)nValid / nTotal:P2}");Console.WriteLine($"Temps brute force : {swBF.Elapsed.TotalMilliseconds:F1} ms");
Brute force - Coloration Australie
=============================================
Combinaisons testees : 2187
Solutions trouvees : 18
Ratio solutions : 0.82 %
Temps brute force : 1.2 ms
Lecture du brute force (cellule ci-dessus) :
La sortie quantifie le coût de l’approche exhaustive : 2187 combinaisons testées pour 18 solutions, soit un ratio de 0,82 % — les solutions sont rares dans l’espace brut, l’essentiel du travail de l’énumération est rejeté par le filtre de cohérence.
Les quelques millisecondes mesurées (valeur qui dépend de la machine, lue dans la sortie ci-dessus) semblent négligeables, mais la croissance est en 3ⁿ : chaque variable ajoutée triple l’espace. À 15 variables, le même balayage vaut ~14 millions de combinaisons ; à 20, ~3,5 milliards — là où le backtracking de la cellule suivante, en coupant des sous-arbres entiers dès la première incohérence partielle, reste dans des tailles polynomiales en pratique sur ces instances.
Les 18 solutions serviront d’étalon : c’est exactement le compte que Choco-solver énumérera en section 6 (cf. la sortie d’énumération complète), ce qui validera la chaîne brute force → backtracking → solveur industriel sur une même instance.
Du brute force au backtracking
Le brute force ci-dessus est correct mais exhaustif : il énumère tout le produit cartésien des domaines, puis filtre les combinaisons valides. Sur la coloration de l’Australie (7 territoires, 3 couleurs), cela fait \(3^7 = 2187\) combinaisons — gérable. Mais sur un graphe à 30 variables et 5 couleurs, on passe à \(5^{30} \approx 9{,}3 \times 10^{20}\) : l’explosion combinatoire rend le brute force inutilisable en pratique.
Le backtracking est la première amélioration fondamentale : au lieu d’énumérer puis de filtrer, on assigne les variables une par une et on vérifie les contraintes dès qu’une variable est fixée. Dès qu’une assignation viole une contrainte, on remonte (backtrack) sans explorer les sous-arbres invalides — on élague ainsi l’immense majorité de l’espace de recherche.
// Backtracking simple pour CSP binaire (C#)// Choisit les variables dans l'ordre de csp.Variables.// Explore les valeurs dans l'ordre du domaine.Dictionary<string,object>Backtrack(CSP csp, Dictionary<string,object> assignment){if(csp.IsComplete(assignment))returnnew Dictionary<string,object>(assignment);var unassigned = csp.Variables.FindAll(v =>!assignment.ContainsKey(v));var var = unassigned[0];foreach(var val in csp.Domains[var]){ csp.GetType();// no-opif(csp.Consistent(var, val, assignment)){ assignment[var]= val;var result =Backtrack(csp, assignment);if(result !=null)return result; assignment.Remove(var);}}returnnull;}// Compteur manuel d'assignations (la classe CSP expose NAssigns via propriété)intCountAssigns(CSP csp, Dictionary<string,object> assignment,refint count){if(csp.IsComplete(assignment))return count;var unassigned = csp.Variables.FindAll(v =>!assignment.ContainsKey(v));var var = unassigned[0];foreach(var val in csp.Domains[var]){ count++;if(csp.Consistent(var, val, assignment)){ assignment[var]= val;int r =CountAssigns(csp, assignment,ref count);if(r >=0)return count; assignment.Remove(var);}}return-1;}// Résolution et mesureaustraliaCSP.ResetCounter();var assignBF =new Dictionary<string,object>();var swBT = Stopwatch.StartNew();var solBT =Backtrack(australiaCSP, assignBF);swBT.Stop();int assignsCount =0;CountAssigns(newCSP(australiaVars, australiaDomains, australiaNeighbors, differentColors),new Dictionary<string,object>(),ref assignsCount);Console.WriteLine("Backtracking simple - Coloration Australie");Console.WriteLine("=============================================");Console.WriteLine($"Solution : {string.Join(",", solBT.Select(kv => kv.Key + "=" + kv.Value))}");Console.WriteLine($"Assignations : ~{assignsCount}");Console.WriteLine($"Temps : {swBT.Elapsed.TotalMilliseconds:F2} ms");Console.WriteLine($"Validation : {(australiaCSP.IsSolution(solBT) ? "VALIDE" : "INVALIDE")}");
Interprétation : le backtracking C# trouve une solution en explorant beaucoup moins de combinaisons que la brute force.
Méthode
Essais
Amelioration
Brute force
2187
référence
Backtracking
~7-15
environ 200x moins
Pourquoi cette réduction ? Le backtracking détecte les inconsistances dès qu elles apparaissent. Si WA = Rouge et NT = Rouge, la contrainte WA != NT est violee immédiatement : toutes les extensions de cette assignation partielle sont eliminees sans être explorees.
Question : peut-on faire encore mieux en choisissant plus intelligemment quelle variable assigner en premier et quelle valeur essayer ?
5. Heuristiques : MRV et LCV (~8 min)
Le backtracking simple fonctionne, mais deux choix stratégiques peuvent considerablement l accelerer :
Quelle variable assigner ensuite ? (variable ordering)
Quelle valeur essayer en premier ? (value ordering)
Principe : choisir la variable qui a le plus petit domaine restant (le moins de valeurs viables).
Intuition (“fail-first”) : si une variable n a que 2 valeurs possibles et une autre en a 10, mieux vaut traiter d abord celle à 2 valeurs. Si elle échoue, on le découvre plus vite, et on élague un sous-arbre plus tôt.
\(\text{MRV}(X_i) = |\{v \in D_i : v \text{ est consistant avec l assignation courante}\}|\)
// Heuristique MRV : choisir la variable avec le moins de valeurs viables.// En cas d'égalité, départage par degree (nombre de voisins non assignés, ordre decroissant).stringSelectMRV(CSP csp, Dictionary<string,object> assignment){var unassigned = csp.Variables.FindAll(v =>!assignment.ContainsKey(v));return unassigned.OrderBy(v => csp.Domains[v].Count(val => csp.Consistent(v, val, assignment))).ThenByDescending(v => csp.Neighbors[v].Count(n =>!assignment.ContainsKey(n))).First();}Console.WriteLine("Heuristique MRV définie.");
Heuristique MRV définie.
Pourquoi MRV réduit l’arbre de recherche
L’heuristique MRV (Minimum Remaining Values) choisit, à chaque étape, la variable non encore assignée qui a le moins de valeurs viables dans son domaine. L’intuition : la variable la plus contrainte est celle qui risque le plus d’échouer — l’assigner en premier fait remonter les échecs plus tôt et élague l’arbre.
La cellule suivante montre comment, après quelques assignations sur l’Australie, MRV sélectionne automatiquement le territoire le plus contraint (celui dont les voisins déjà colorés réduisent le plus son domaine), plutôt que de suivre l’ordre naturel de déclaration des variables.
// Démonstration du choix MRV après quelques assignationsvar demoCSP =newCSP(australiaVars, australiaDomains, australiaNeighbors, differentColors);var partial =new Dictionary<string,object>{{"WA","Rouge"},{"NT","Vert"}};Console.WriteLine("Assignation partielle : WA=Rouge, NT=Vert");Console.WriteLine();Console.WriteLine($"{"Variable",-8} {"Valeurs viables",-32} {"MRV",5} {"Deg",5}");Console.WriteLine(newstring('-',55));string chosen =SelectMRV(demoCSP, partial);foreach(var v in demoCSP.Variables){if(!partial.ContainsKey(v)){var viable = demoCSP.Domains[v].Where(val => demoCSP.Consistent(v, val, partial)).ToList();int deg = demoCSP.Neighbors[v].Count(n =>!partial.ContainsKey(n));string marker = v == chosen ?" <-- MRV":""; Console.WriteLine($"{v,-8} {string.Join(",", viable),-32} {viable.Count,5} {deg,5}{marker}");}}
Interprétation : MRV choisit SA (Southern Australia) parce qu’il ne lui reste qu’une seule valeur viable (Bleu) – c’est la variable la plus contrainte du graphe. C’est le principe fail-fast : traiter en priorite la variable la plus susceptible de provoquer une impasse, afin de detecter l’échec tot dans l’arbre de recherche et d’éviter d’explorer de larges sous-arbres voués à l’échec.
Au tie-break (plusieurs variables avec le même nombre de valeurs viables), le degree heuristic départage en choisissant celle qui a le plus de voisins non assignés –limiter ainsi la propagation future des contraintes.
Sur ce petit graphe de coloration, l’effet de MRV reste modeste ; il devient decisif sur des CSP denses ou une variable presqu’entièrement determinee (viables == 1) dechoue immédiatement si on la traite en dernier.
MRV répond à la première question (quel ordre de variables ?). Reste la seconde : parmi les valeurs viables d’une variable, quel ordre essayer ? C’est le rôle complémentaire de LCV (Least Constraining Value) – présente dans la cellule suivante.
5.2 Value Ordering : LCV (Least Constraining Value)
Principe : pour la variable choisie (par MRV), essayer d’abord la valeur qui élimine le moins de possibilités pour les variables voisines non assignées.
Intuition (“succeed-first”) : à l’inverse du « fail-first » pour les variables, pour les valeurs on préfère maximiser les chances que le reste du problème reste soluble — on garde le maximum de flexibilité pour la suite de la recherche.
\[\text{LCV}(v) = \sum_{\text{voisin } Y \text{ non assigné}} |\{w \in D_Y : \text{contrainte}(X, v, Y, w) \text{ violée}\}|\]
On trie les valeurs de \(D_X\) par \(\text{LCV}(v)\)croissant (la moins contraignante d’abord). L’implémentation C# correspondante est la méthode OrderLCV dans la cellule suivante.
// Heuristique LCV : trier les valeurs par nombre de conflits croissant// (la moins contraignante d'abord -- "succeed-first")List<object>OrderLCV(CSP csp,string var, Dictionary<string,object> assignment){return csp.Domains[var].OrderBy(val =>{int count =0;foreach(var neighbor in csp.Neighbors[var]){if(!assignment.ContainsKey(neighbor)){foreach(var nval in csp.Domains[neighbor]){if(!csp.ConstraintFunc(var, val, neighbor, nval)) count++;}}}return count;}).ToList();}Console.WriteLine("Heuristique LCV définie.");
Heuristique LCV définie.
Combiner les heuristiques : MRV + LCV
Nous avons jusqu’ici deux heuristiques orthogonales : - MRV décide quelle variable assigner ensuite — la plus contrainte, - LCV (Least Constraining Value) décide quelle valeur tester en premier — celle qui élimine le moins d’options pour les voisins.
Un backtracking efficace les combine : MRV pour le choix de variable, LCV pour l’ordre des valeurs. Les deux réduisent l’arbre de façon complémentaire — MRV en accélérant les échecs, LCV en privilégiant les branches qui aboutissent. La cellule suivante implémente cette combinaison.
// Backtracking amélioré : MRV + LCV combines// Note : en C#, les paramètres ref doivent être places après les paramètres optionnels// (et après tous les paramètres requis). On utilise une fonction wrapping qui retourne// le compte via une closure (ref int via wrapper local).Dictionary<string,object>BacktrackImproved(CSP csp, Dictionary<string,object> assignment,bool useMRV,bool useLCV){if(csp.IsComplete(assignment))returnnew Dictionary<string,object>(assignment);string var;if(useMRV){ var =SelectMRV(csp, assignment);}else{ var = csp.Variables.Find(v =>!assignment.ContainsKey(v));}var values = useLCV ?OrderLCV(csp, var, assignment): csp.Domains[var];foreach(var val in values){if(csp.Consistent(var, val, assignment)){ assignment[var]= val;var result =BacktrackImproved(csp, assignment, useMRV, useLCV);if(result !=null)return result; assignment.Remove(var);}}returnnull;}intCountAssignsRun(CSP csp,bool useMRV,bool useLCV){int count =0;var localAssign =new Dictionary<string,object>();// Compteur via wrapper recurif (compte avant chaque consistent check)voidCounter(Dictionary<string,object> a){if(csp.IsComplete(a))return;var v = useMRV ?SelectMRV(csp, a): csp.Variables.Find(x =>!a.ContainsKey(x));var values = useLCV ?OrderLCV(csp, v, a): csp.Domains[v];foreach(var val in values){ count++;if(csp.Consistent(v, val, a)){ a[v]= val;Counter(a); a.Remove(v);}}}Counter(localAssign);return count;}Console.WriteLine("Backtracking amélioré défini (variantes contrôlées par useMRV/useLCV booléens).");
Backtracking amélioré défini (variantes contrôlées par useMRV/useLCV booléens).
Comparaison empirique des variantes
Nous disposons maintenant de trois stratégies pour le même problème (coloration de l’Australie) : 1. Brute force — référence exhaustive (lente mais complète), 2. Backtracking simple — assignation séquentielle avec vérification immédiate des contraintes, 3. Backtracking MRV + LCV — backtracking enrichi des deux heuristiques.
La comparaison suivante mesure le nombre d’assignations testées par chaque variante. C’est la métrique qui révèle le gain réel des heuristiques : moins d’assignations = moins de nœuds explorés = arbre plus petit. Sur un petit graphe l’écart peut sembler modeste, mais il devient drastique quand le nombre de variables croît.
// Comparaison des variantes sur la coloration de l'Australievar variants =new(string name,bool mrv,bool lcv)[]{("Backtracking simple",false,false),("+ MRV",true,false),("+ MRV + LCV",true,true),};var results =new List<(string name,int assigns,double ms)>();Console.WriteLine("Comparaison des variantes - Coloration Australie");Console.WriteLine("=======================================================");Console.WriteLine($"{"Variante",-22} {"Assignations",12} {"Temps(ms)",12}");Console.WriteLine(newstring('-',50));foreach(var v in variants){var csp =newCSP(australiaVars, australiaDomains, australiaNeighbors, differentColors);var assign =new Dictionary<string,object>();var sw = Stopwatch.StartNew();// BacktrackImproved n utilise plus ref int ; on utilise CountAssignsRun pour le compteint n =CountAssignsRun(csp, v.mrv, v.lcv);var sol =BacktrackImproved(csp,new Dictionary<string,object>(), v.mrv, v.lcv); sw.Stop(); results.Add((v.name, n, sw.Elapsed.TotalMilliseconds)); Console.WriteLine($"{v.name,-22} {n,12} {sw.Elapsed.TotalMilliseconds,12:F2}");}
Interprétation : sur ce petit graphe, les heuristiques n amelioresent pas le compte d assignations.
Variante
Assignations
Commentaire
Backtracking simple
102
Ordre naif des variables et valeurs
+ MRV
102
Même arbre explore, aucun élagage
+ MRV + LCV
102
idem, LCV ne change rien au compte
Points clés : 1. Sur ce petit problème (7 variables, 3 couleurs, espace déjà très contraint), l ordre naif tombe déjà sur une solution ; les heuristiques n elaguent aucun noeud - l arbre de recherche est identique (102 noeuds pour les trois variantes) 2. L effet des heuristiques est instance-dependant : nul sur une instance facile, il devient dramatique sur des instances plus dures (cf. les 8-Reines ci-dessous : 876 -> 572 avec MRV) 3. Conclusion honnete : aucune heuristique n est monotonement benefique, il faut mesurer et non presupposer une amelioration
Règle pratique : mesurer empiriquement. MRV rapporte gros sur les instances dures (N-Reines), mais sur une instance facile comme la coloration de l’Australie, l arbre de recherche est si petit que les heuristiques n ont rien à élaguer.
6. Solveur SOTA : Choco-solver 4.10.17 (~10 min)
Jusqu ici nous avons tout implémenté à la main en C#. En pratique, on utilise un solveur CSP optimisé comme Choco-solver 4.10.17 (open-source, leader en recherche operationnelle).
Pourquoi Choco-solver ?
Solveur SOTA : Choco-solver est un solveur CSP open-source de référence en Java, maintenu par l équipe CHOCTeam (labo TASC INRIA, Nantes)
Solveur complet + optimisation : backtracking avec propagation de contraintes, heuristiques integrees (MRV, LCV, impact-based, …)
API Java exposee via IKVM : toutes les classes Java (Model, IntVar, IntConstraintFactory, …) sont accessibles directement depuis C#
Installation
Pas d installation requise ici : la DLL Choco 4.10.17 est pré-compilée dans le dossier Part2-CSP, et IKVM 8.15.0 est chargé à la volée via NuGet au debut du notebook.
// Coloration de l'Australie avec Choco-solver 4.10.17// Comparaison directe avec notre implémentation manuellevar chocModel =newModel("Coloration Australie - Choco 4.10.17");// Variables : IntVar par état, domaine [0..2]var chocVars =new Dictionary<string, IntVar>();foreach(var v in australiaVars){ chocVars[v]= chocModel.intVar(v,0,2);}// Contraintes : régions adjacentes différentesforeach(var v1 in australiaVars){foreach(var v2 in australiaNeighbors[v1]){ chocModel.arithm(chocVars[v1],"!=", chocVars[v2]).post();}}// Résolution : Choco 4.10.17 utilise solver.solve() (retourne bool), pas findSolution()var chocSolver = chocModel.getSolver();var swChoco = Stopwatch.StartNew();bool chocOk = chocSolver.solve();swChoco.Stop();var colorNames =new[]{"Rouge","Vert","Bleu"};Console.WriteLine("Choco-solver - Coloration Australie");Console.WriteLine("=============================================");if(chocOk){ Console.WriteLine("Solution : {");foreach(var v in australiaVars){int idx = chocVars[v].getValue(); Console.WriteLine($" {v,-4} = {colorNames[idx]} ({idx})");} Console.WriteLine("}"); Console.WriteLine($"Temps : {swChoco.Elapsed.TotalMilliseconds:F2} ms");}else{ Console.WriteLine("Pas de solution.");}
Choco-solver - Coloration Australie
=============================================
Solution : {
WA = Rouge (0)
NT = Vert (1)
SA = Bleu (2)
Q = Rouge (0)
NSW = Vert (1)
V = Rouge (0)
T = Rouge (0)
}
Temps : 55.56 ms
Interprétation : Choco-solver résout la coloration de l’Australie en un temps mesuré en sortie au premier appel, puis beaucoup plus rapidement une fois la JVM chaude (cf. énumération cellule suivante). Le premier appel paie le demarrage de la JVM Java (IKVM) sous-jacente ; en regime etabli, le coeur metier (propagation + backtracking) est quasi-instantane (runtime machine-dep, cf. la mesure en sortie de la cellule suivante).
Aspect
Implementation manuelle C#
Choco-solver 4.10.17
Concision de la modélisation
Plus verbeuse (classe CSP + voisins à écrire)
Plus concise
Heuristiques integrees
A implémenter
MRV, LCV, impact-based, …
Propagation de contraintes
Non (vérification à chaque appel)
AC-3 intégré
Solveur SOTA
Non (brute force / backtracking)
Oui (publication CHOCTeam 2008-2024)
Points clés : 1. L API Choco est déclarative : on declare variables + contraintes, le solveur fait le reste 2. model.arithm(v1, "!=", v2).post() est l équivalent exact de notre Consistent(var, val, assignment) 3. solver.solve() intègre backtracking + heuristiques + propagation (retourne bool ; la solution est accessible via var.getValue())
// Enumeration de TOUTES les solutions avec Choco (record & enumerate)// Equivalent du getSolutions() de python-constraintvar chocModel2 =newModel("Coloration Australie - énumération");var chocVars2 =new Dictionary<string, IntVar>();foreach(var v in australiaVars){ chocVars2[v]= chocModel2.intVar(v,0,2);}foreach(var v1 in australiaVars){foreach(var v2 in australiaNeighbors[v1]){ chocModel2.arithm(chocVars2[v1],"!=", chocVars2[v2]).post();}}var swEnum = Stopwatch.StartNew();// Choco 4.10.17 : énumération via boucle while solve()var enumSolver = chocModel2.getSolver();int totalSolutions =0;var firstSolutions =new List<Dictionary<string,int>>();while(enumSolver.solve()){var one =new Dictionary<string,int>();foreach(var v in australiaVars) one[v]= chocVars2[v].getValue();if(firstSolutions.Count<5) firstSolutions.Add(one); totalSolutions++;}swEnum.Stop();Console.WriteLine($"Nombre total de solutions : {totalSolutions}");Console.WriteLine($"Temps énumération : {swEnum.Elapsed.TotalMilliseconds:F2} ms");Console.WriteLine();Console.WriteLine("Premières 5 solutions :");int k =0;foreach(var sol in firstSolutions){var dict =new List<string>();foreach(var v in australiaVars) dict.Add($"{v}={colorNames[sol[v]]}"); Console.WriteLine($" Solution {k + 1} : {{ {string.Join(",", dict)} }}"); k++;}
Interprétation : Choco énumère les 18 solutions valides (comme la brute force, mais avec propagation de contraintes en interne).
Aspect
Brute force Python
Choco findAllSolutions()
Combinaisons testees
2187
Reduit (propagation AC-3)
Solutions
18
18
Temps
machine-dep (cf. cellule de mesure brute force)
machine-dep (mesure en sortie de la cellule précédente)
Pourquoi 18 solutions ? Les couleurs Rouge/Vert/Bleu sont interchangeables (symétrie de permutation). Sans sym-breaking explicite, Choco énumère toutes les solutions symetriques. Pour une problematique de coloration “recherche d une solution”, solver.solve() (qui retourne true/false) suffit.
6.1 Démonstration SOTA : N-Reines avec Choco
Pour montrer l apport de Choco sur une instance dure, résolvez les 8-Reines (placer 8 reines sur un échiquier sans qu aucune ne s attaque). Notre implémentation manuelle a nécessité ~876 assignations pour les 8-Reines. Choco devrait faire mieux grâce à ses heuristiques internes et la propagation de contraintes.
// 8-Reines avec Choco-solver// Variables : une par colonne (0..7), domaine 0..7 (ligne)// Contraintes : pas même ligne, pas même diagonaleint n =8;var queenModel =newModel($"N-Reines (n={n})");var queens =new IntVar[n];for(int c =0; c < n; c++){// Cast explicite (string) pour éviter l'ambiguïté entre intVar(int,int) et intVar(int,int,bool) queens[c]= queenModel.intVar(c.ToString(),0, n -1);}// AllDifferent : toutes les lignes différentes (1 seule reine par ligne)queenModel.allDifferent(queens).post();// Diagonales : pour chaque paire (i,j), créer une IntVar auxiliaire pour la diff/somme,// puis la comparer à la constante. Choco 4.10.17 surcharge `arithm(IntVar,String,IntVar,String,IntVar)`// pas forcement portee par l'IKVM Java->.NET ; on utilise des contraintes separees qui sont// bien supportees : arithm(var, "op", var) + arithm(IntVarResult, "op", int).for(int i =0; i < n; i++){for(int j = i +1; j < n; j++){// Diagonale descendante : q_i - q_j != i - j// Cast explicite (string) sur le nom pour éviter l'ambiguïté intVar(int,int,bool)var diff = queenModel.intVar($"d_{i}_{j}",-(n -1), n -1); queenModel.scalar(new IntVar[]{ queens[i], queens[j]},newint[]{1,-1},"=", diff).post();int diagDown = i - j; queenModel.arithm(diff,"!=", diagDown).post();// Diagonale montante : q_i + i != q_j + j <=> q_i - q_j != j - ivar diffUp = queenModel.intVar($"u_{i}_{j}",-(n -1), n -1); queenModel.scalar(new IntVar[]{ queens[i], queens[j]},newint[]{1,-1},"=", diffUp).post();int diagUp = j - i; queenModel.arithm(diffUp,"!=", diagUp).post();}}var swQueen = Stopwatch.StartNew();// Choco 4.10.17 : solve() (bool) au lieu de findSolution()var queenSolver = queenModel.getSolver();bool queenOk = queenSolver.solve();swQueen.Stop();if(queenOk){ Console.WriteLine($"Solution {n}-Reines :"); Console.WriteLine($" (col -> ligne) : {string.Join(",", Enumerable.Range(0, n).Select(c => $"{c}->{queens[c].getValue()}"))}"); Console.WriteLine($"Temps : {swQueen.Elapsed.TotalMilliseconds:F2} ms"); Console.WriteLine(); Console.WriteLine("Echiquier (Q = reine) :");for(int row = n -1; row >=0; row--){string line = $" Ligne {row} | ";for(int col =0; col < n; col++){ line += queens[col].getValue()== row ?" Q ":" . ";} Console.WriteLine(line);}}else{ Console.WriteLine("Pas de solution trouvee.");}
Interprétation : Choco-solver résout les 8-Reines quasi-instantanément (runtime machine-dep, temps mesure en sortie de la cellule précédente).
Mesure
Implementation manuelle
Choco-solver
Assignations
~876
non expose mais << 876 (propagation + heuristiques)
Code de modélisation
plus long (classe CSP + contraintes)
plus court
Heuristiques
A choisir (MRV, LCV)
Integree (impact-based par defaut)
Propagation
Non (vérification manuelle)
AC-3 intégrée
Points clés : 1. Choco intègre nativement propagation de contraintes (AC-3) + heuristiques avancees (impact-based, ABS, …) 2. Le code est plus court et plus expressif 3. Sur des instances plus grandes (16-Reines, 32-Reines), Choco résout ou détecte l infaisabilité alors qu une implémentation manuelle brute-force explose
Règle pratique : pour des problemes réels (N > 20 variables), utiliser Choco. Pour apprendre les mécanismes, l implémentation manuelle reste pédagogique.
6.2 Contrainte globale : AllDifferent avec Choco
La cellule 6.1 ci-dessus utilise déjà queenModel.allDifferent(queens).post() – une contrainte globale imposée à un tableau de 8 variables : toutes les lignes doivent être distinctes (1 seule reine par ligne). Cette section isole cette contrainte pour bien montrer son pouvoir d’élagage et la comparer à une modélisation binaire q[i] != q[j] (6 contre 1).
Pourquoi “globale” ? Au lieu de coder \(O(n^2)\) contraintes binaires xi != xj (qui laissent le solveur explorer tout l’espace par backtracking), allDifferent est traitée comme une seule contrainte par Choco via une structure de couplage (matching biparti, complexité \(O(n\sqrt{n})\) – cf. Regin 1994, A Filtering Algorithm for Constraints of Difference in CSPs, JAIR). Sur 4 variables de domaine {1..4}, le nombre de solutions tombe directement à \(4! = 24\)sans aucun backtracking sur cette contrainte.
Parité pédagogique avec le twin Python : la jumeau Python (CSP-1-Fundamentals.ipynb, section AllDifferentConstraint) utilise la bibliothèque python-constraint et expose la même contrainte via Problem.addConstraint(AllDifferentConstraint(), variables). La formulation Choco est plus déclarative (un seul appel) et intégrée au solveur ; la formulation Python est plus pédagogique (le constructeur AllDifferentConstraint est explicite, on voit comment il s’instancie). Même concept, deux implémentations SOTA.
Exemple illustratif – allDifferent sur 4 variables {A, B, C, D} de domaine {1, 2, 3, 4} (les 24 permutations) :
// Modèle jouet : 4 variables, domaine [1..4], toutes différentesvar adModel =newModel("AllDifferent demo - 4 vars");var letters =new[]{"A","B","C","D"};var adVars =new Dictionary<string, IntVar>();foreach(var name in letters) adVars[name]= adModel.intVar(name,1,4);// Contrainte globale : matching biparti intégré (pas de backtracking sur cette contrainte)adModel.allDifferent(adVars.Values.ToArray()).post();// Enumeration de TOUTES les solutions (pattern Choco 4.10.17 solve() en boucle)var adSolver = adModel.getSolver();int solutionCount =0;while(adSolver.solve()){// 24 appels successifs solutionCount++; Console.WriteLine(string.Join(", ", letters.Select(L => $"{L}={adVars[L].getValue()}")));}Console.WriteLine($"Nombre de solutions : {solutionCount} (= 4! = 24)");
Sortie attendue : 24 lignes de la forme A=1, B=2, C=3, D=4 … A=4, B=3, C=2, D=1 (toutes les permutations de {1,2,3,4}). Pour la runnable end-to-end, voir le twin Python (CSP-1-Fundamentals.ipynb, cellule Démonstration AllDifferentConstraint) qui utilise python-constraint (même résultat : \(4! = 24\) solutions).
Aspect
xi != xj binaires (pairwise)
allDifferent global
Contraintes posee
6 (!=)
1 (globale)
Propagation
Vérification à chaque assignation
Filtrage par matching biparti
Backtracking sur cette contrainte
oui (n! explorations)
non (0 backtracking sur 4 vars)
Memoire
Aucune
Table de couplage \(O(n^2)\)
Complexite pire cas
\(O(n!)\)
\(O(n\sqrt{n})\) (matching)
Points clés :
allDifferent est la façon idiomatique de déclarer une “toutes différentes” en Choco – toujours préférer cette formulation aux contraintes binaires équivalentes
Sudoku (lignes/colonnes/blocs), N-Reines (lignes), coloration équitable, planning de personnel, ordre de passage : c’est la première contrainte globale à apprendre
Choco expose aussi model.allDifferentExcept (relaxation avec liste d’exceptions) – utile pour les variantes pedagogiques
Référence : Choco-solver 4.10.17 documentation, section Global Constraints ; implémentation par matching biparti (cf. Regin 1994, A Filtering Algorithm for Constraints of Difference in CSPs, J. Artif. Intell. Res. 1).
Énoncé : modélisez et résolvez le problème des 4-Reines avec votre propre implémentation C# (classe CSP + BacktrackImproved avec MRV + LCV). Affichez la solution comme une matrice 4x4 avec un caractère Q pour les reines et un . pour les cases vides.
Indice : la modélisation est la même que pour les 8-Reines Choco de la section 6.1, mais vous devez utiliser votre propre classe CSP. La contrainte est : deux reines ne peuvent pas être sur la même ligne, colonne ou diagonale. Les variables sont les colonnes, le domaine est l ensemble des lignes {0, 1, 2, 3}.
Sortie attendue : 2 solutions symetriques (l une est le miroir de l autre).
// Exercice 1 : 4-Reines avec backtracking manuel C#// A COMPLETER par l'étudiant// Indice : utilisez la classe CSP et BacktrackImproved définies plus haut.// Modelisation : 4 variables (colonnes), domaine {0,1,2,3} (lignes),// contrainte = reine sur (c1, r1) et (c2, r2) ne s attaquent pas.// Conditions : r1 != r2 (même ligne) ET |c1-c2| != |r1-r2| (même diagonale).Console.WriteLine("Exercice à compléter : 4-Reines avec backtracking manuel C#");Console.WriteLine("Solution attendue : 2 solutions symetriques.");
Exercice à compléter : 4-Reines avec backtracking manuel C#
Solution attendue : 2 solutions symetriques.
Exercice 2 : Comparer MRV sur les 4-Reines (manuel vs Choco)
Énoncé : résolvez les 4-Reines sans MRV (useMRV=false) et avec MRV (useMRV=true) en utilisant votre implémentation BacktrackImproved. Comparez le nombre d assignations. Ensuite, résolvez les mêmes 4-Reines avec Choco-solver et comparez les temps.
Indice : créez un CSP avec 4 variables (colonnes), domaine {0,1,2,3} (lignes), et une contrainte reine-non-attaque. Utilisez BacktrackImproved(csp, assignment, useMRV, useLCV) pour la recherche et CountAssignsRun(csp, useMRV, useLCV) pour mesurer le nombre d’assignations. Pour Choco, utilisez IntVar + allDifferent + arithm comme en section 6.1.
Sortie attendue : un tableau comparatif avec 3 lignes (manuel sans MRV, manuel avec MRV, Choco) et 2 colonnes (assignations, temps).
// Exercice 2 : comparer MRV sur les 4-Reines// A COMPLETER par l'étudiant// Indice : mesurez assignations et temps pour 3 variantes :// 1) Manuel sans MRV : BacktrackImproved(csp, ..., useMRV=false, useLCV=false)// 2) Manuel avec MRV : BacktrackImproved(csp, ..., useMRV=true, useLCV=false)// 3) Choco : Model + IntVar + allDifferent + arithmConsole.WriteLine("Exercice de benchmark - decommentez pour tester");
Exercice de benchmark - decommentez pour tester
Exercice 3 : SEND + MORE = MONEY avec Choco-solver
Énoncé : le puzzle cryptarithmétique SEND + MORE = MONEY consiste à trouver l affectation de chiffres (0-9) aux lettres telle que l addition soit correcte. Chaque lettre représente un chiffre distinct, et S et M ne peuvent pas être 0 (pas de zéro initial).
S E N D
+ M O R E
---------
M O N E Y
Modélisez et résolvez ce problème avec Choco-solver.
Indice : - 8 variables (S, E, N, D, M, O, R, Y), domaine 0..9 - Contrainte allDifferent(S,E,N,D,M,O,R,Y) - Contraintes S != 0 et M != 0 : chocoModel.arithm(s, "!=", 0).post() - Contrainte arithmetique : \(1000 \times S + 100 \times E + 10 \times N + D + 1000 \times M + 100 \times O + 10 \times R + E = 10000 \times M + 1000 \times O + 100 \times N + 10 \times E + Y\)
Sortie attendue : la solution classique est 9567 + 1085 = 10652 (S=9, E=5, N=6, D=7, M=1, O=0, R=8, Y=2).
// Exercice 3 : SEND + MORE = MONEY avec Choco-solver// A COMPLETER par l'étudiant// Indice :// - 8 IntVar (s,e,n,d,m,o,r,y), domaine 0..9// - model.allDifferent(new IntVar[] {s,e,n,d,m,o,r,y}).post()// - model.arithm(s, "!=", 0).post() ET model.arithm(m, "!=", 0).post()// - Contrainte arithmetique : 1000*s + 100*e + 10*n + d + 1000*m + 100*o + 10*r + e// = 10000*m + 1000*o + 100*n + 10*e + yConsole.WriteLine("Exercice SEND + MORE = MONEY - decommentez pour tester");
Exercice SEND + MORE = MONEY - decommentez pour tester
Et ensuite ?
Ce notebook a couvert les fondamentaux des CSP en C# et introduit Choco-solver 4.10.17 comme solveur SOTA via IKVM 8.15.0. Le notebook suivant, CSP-2-Consistance, introduit les techniques de propagation de contraintes avancees :
Forward Checking : eliminer les valeurs inconsistantes des voisins à chaque assignation
Arc Consistency (AC-3) : rendre le CSP arc-consistent avant et pendant la recherche
MAC (Maintaining Arc Consistency) : combiner AC-3 avec le backtracking
Ces techniques permettent de réduire encore davantage l exploration en détectant les impasses plus tôt.
References
Russell, S. & Norvig, P. Artificial Intelligence: A Modern Approach, Chapitre 6
Dechter, R. Constraint Processing, Cambridge University Press, 2003
Voir aussi la série Sudoku pour une application complète des CSP
Conclusion
Ce notebook a introduit les problemes de satisfaction de contraintes (CSP) en C#, avec une comparaison directe entre implémentation manuelle (classe CSP maison + backtracking + heuristiques MRV/LCV) et solveur SOTA (Choco-solver 4.10.17 via IKVM).
Concepts clés
Concept
Description
CSP
Problème défini par variables, domaines et contraintes