CSP-1 : Fondamentaux des CSP - Version .NET (Choco-solver 4.10.17 via IKVM 8.15.0)

Navigation : << Search-5 GeneticAlgorithms | Index | CSP-2-Consistance >> | Version Python

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)

Duree estimee : 50 minutes

Lien avec d autres series


1. Configuration de l environnement (.NET Interactive + IKVM + Choco)

Architecture

Pour executer du code Choco-solver (Java) depuis C# sous .NET Interactive, nous utilisons la chaîne :

  .NET Interactive (kernel .net-csharp)
       --> IKVM 8.15.0   (compilateur Java -> .NET)
            --> org.chocosolver.solver.dll   (Choco 4.10.17 pré-compilée)

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ée
using System;
using System.IO;

string FindCspDir() {
    var dir = new DirectoryInfo(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 = new FileInfo(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) {
    throw new FileNotFoundException("DLL Choco introuvable", dllPath);
}
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);

void IkvmCopyMerge(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");
Choco-solver 4.10.17 charge -- Model, IntVar, arithm, allDifferent disponibles

2. Formalisation CSP (~5 min)

Définition

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;

public class 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; }
    public int NAssigns { get; private set; }

    public CSP(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;
    }

    public bool Consistent(string var, object val, Dictionary<string, object> assignment) {
        if (!Neighbors.TryGetValue(var, out var neigh)) return true;
        foreach (var otherVar in neigh) {
            if (assignment.TryGetValue(otherVar, out var otherVal)) {
                if (!ConstraintFunc(var, val, otherVar, otherVal)) return false;
            }
        }
        return true;
    }

    public bool IsComplete(Dictionary<string, object> assignment)
        => assignment.Count == Variables.Count;

    public bool IsSolution(Dictionary<string, object> assignment) {
        if (!IsComplete(assignment)) return false;
        foreach (var v in Variables) {
            if (!Consistent(v, assignment[v], assignment)) return false;
        }
        return true;
    }

    public void ResetCounter() => 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 binaire
var 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 = new CSP(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 :

  1. Choisir une variable non encore assignee
  2. 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
  3. 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 backtracking
using System.Diagnostics;

var colors = new List<object> { "Rouge", "Vert", "Bleu" };
var allCombos = new List<Dictionary<string, object>>();

// produit cartesien des 7 domaines
void GenerateCombos(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)) return new 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-op
        if (csp.Consistent(var, val, assignment)) {
            assignment[var] = val;
            var result = Backtrack(csp, assignment);
            if (result != null) return result;
            assignment.Remove(var);
        }
    }
    return null;
}

// Compteur manuel d'assignations (la classe CSP expose NAssigns via propriété)
int CountAssigns(CSP csp, Dictionary<string, object> assignment, ref int 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 mesure
australiaCSP.ResetCounter();
var assignBF = new Dictionary<string, object>();
var swBT = Stopwatch.StartNew();
var solBT = Backtrack(australiaCSP, assignBF);
swBT.Stop();
int assignsCount = 0;
CountAssigns(new CSP(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")}");
Backtracking simple - Coloration Australie
=============================================
Solution         : WA=Rouge, NT=Vert, SA=Bleu, Q=Rouge, NSW=Vert, V=Rouge, T=Rouge
Assignations     : ~11
Temps            : 0.53 ms
Validation       : VALIDE

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 :

  1. Quelle variable assigner ensuite ? (variable ordering)
  2. Quelle valeur essayer en premier ? (value ordering)

5.1 Variable Ordering : MRV (Minimum Remaining Values)

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).
string SelectMRV(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 assignations
var demoCSP = new CSP(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(new string('-', 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}");
    }
}
Assignation partielle : WA=Rouge, NT=Vert

Variable Valeurs viables                    MRV   Deg
-------------------------------------------------------
SA       Bleu                                 1     3 <-- MRV
Q        Rouge,Bleu                           2     2
NSW      Rouge,Vert,Bleu                      3     3
V        Rouge,Vert,Bleu                      3     2
T        Rouge,Vert,Bleu                      3     0

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)) return new 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);
        }
    }
    return null;
}

int CountAssignsRun(CSP csp, bool useMRV, bool useLCV) {
    int count = 0;
    var localAssign = new Dictionary<string, object>();
    // Compteur via wrapper recurif (compte avant chaque consistent check)
    void Counter(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'Australie
var 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(new string('-', 50));

foreach (var v in variants) {
    var csp = new CSP(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 compte
    int 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}");
}
Comparaison des variantes - Coloration Australie
=======================================================
Variante               Assignations   Temps (ms)
--------------------------------------------------
Backtracking simple             102         1.08
+ MRV                           102         0.41
+ MRV + LCV                     102         1.03

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 manuelle
var chocModel = new Model("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érentes
foreach (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-constraint
var chocModel2 = new Model("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++;
}
Nombre total de solutions : 18
Temps énumération          : 3.85 ms

Premières 5 solutions :
  Solution 1 : { WA=Rouge, NT=Vert, SA=Bleu, Q=Rouge, NSW=Vert, V=Rouge, T=Rouge }
  Solution 2 : { WA=Rouge, NT=Vert, SA=Bleu, Q=Rouge, NSW=Vert, V=Rouge, T=Vert }
  Solution 3 : { WA=Rouge, NT=Vert, SA=Bleu, Q=Rouge, NSW=Vert, V=Rouge, T=Bleu }
  Solution 4 : { WA=Rouge, NT=Bleu, SA=Vert, Q=Rouge, NSW=Bleu, V=Rouge, T=Rouge }
  Solution 5 : { WA=Rouge, NT=Bleu, SA=Vert, Q=Rouge, NSW=Bleu, V=Rouge, T=Vert }

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 diagonale
int n = 8;
var queenModel = new Model($"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] }, new int[] { 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 - i
        var diffUp = queenModel.intVar($"u_{i}_{j}", -(n - 1), n - 1);
        queenModel.scalar(new IntVar[] { queens[i], queens[j] }, new int[] { 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.");
}
Solution 8-Reines :
  (col -> ligne) : 0->4, 1->6, 2->1, 3->5, 4->2, 5->0, 6->3, 7->7
Temps            : 18.68 ms

Echiquier (Q = reine) :
  Ligne 7 |  .  .  .  .  .  .  .  Q 
  Ligne 6 |  .  Q  .  .  .  .  .  . 
  Ligne 5 |  .  .  .  Q  .  .  .  . 
  Ligne 4 |  Q  .  .  .  .  .  .  . 
  Ligne 3 |  .  .  .  .  .  .  Q  . 
  Ligne 2 |  .  .  .  .  Q  .  .  . 
  Ligne 1 |  .  .  Q  .  .  .  .  . 
  Ligne 0 |  .  .  .  .  .  Q  .  . 

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érentes
var adModel = new Model("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 :

  1. 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
  2. Sudoku (lignes/colonnes/blocs), N-Reines (lignes), coloration équitable, planning de personnel, ordre de passage : c’est la première contrainte globale à apprendre
  3. 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).


Recapitulatif et exercices

Tableau recapitulatif

Approche Noeuds explores Temps Facilité d utilisation
Brute force \(\prod \vert D_i\vert\) (tous) Lent Simple mais intraitable
Backtracking simple C# Reduit (élagage) Rapide Implementation modérée
Backtracking + MRV Très réduit Très rapide Implementation modérée
Backtracking + MRV + LCV Minimal Très rapide Plus complexe à implémenter
Choco-solver 4.10.17 Optimal (propagation AC-3) Très rapide API déclarative

Concepts clés à retenir

Concept Définition Implementation
Variable (\(X_i\)) Quantité à déterminer IntVar (Choco) / string (manuel)
Domaine (\(D_i\)) Valeurs possibles pour \(X_i\) intVar(name, min, max) (Choco) / List<object> (manuel)
Contrainte (\(C_k\)) Restriction sur sous-ensemble arithm(!=).post() (Choco) / ConstraintFunc (manuel)
Solution Assignation complete et consistante solver.solve() (Choco) / IsSolution() (manuel)
MRV Variable au domaine le plus restreint SelectMRV() (manuel) / intégré Choco
LCV Valeur la moins contraignante OrderLCV() (manuel) / intégré Choco

Exercice 1 : 4-Reines avec backtracking manuel C

É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 + arithm

Console.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 + y

Console.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
  • Choco-solver documentation – solveur SOTA utilise dans ce notebook
  • 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
Graphe de contraintes Representation : noeuds = variables, arêtes = contraintes
Backtracking Recherche depth-first avec retour arrière sur échec
Brute force Enumeration exhaustive de toutes les combinaisons
MRV / LCV Heuristiques fail-first / succeed-first
Choco-solver Solveur SOTA avec propagation AC-3 et heuristiques integrees

Algorithmes de résolution CSP

Algorithme Principe Efficacité Utilisation
Brute force Enumeration \(O(d^n)\) Problèmes tiny (n < 10)
Backtracking simple Retour arrière \(O(d^n)\) élague Problèmes petits (n < 20)
Backtracking + AC-3 Propagation Reduit espace Problèmes moyens (n < 50)
Choco-solver SOTA + propagation Très rapide Production, recherche

Points clés à retenir

  1. La modélisation CSP separe le problème du langage de résolution
  2. Le backtracking est l algorithme de base pour tous les solveurs CSP
  3. Choco-solver intègre propagation + heuristiques : à utiliser pour les problèmes réels
  4. L implémentation manuelle reste pédagogique : elle fait comprendre les mécanismes
  5. Les applications sont nombreuses : sudoku, emploi du temps, configuration, vérification

Voir aussi : - CSP-2-Consistency.ipynb pour les algorithmes de consistance (AC-3, forward checking) - CSP-3-Advanced-CSharp.ipynb - Contraintes globales et optimisation multi-objectif

Retour au sommet