App-1 : Le Problème des N-Reines

# Parameters
BATCH_MODE = "true"

Navigation : Index | Part2-CSP | App-2 GraphColoring >>

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Formaliser le problème des N-Reines comme un CSP (variables, domaines, contraintes) – Bloom : Comprendre 2. Implementer un solveur par backtracking avec heuristiques (MRV, Forward Checking) – Bloom : Appliquer 3. Comparer backtracking, min-conflicts et OR-Tools CP-SAT sur différentes tailles – Bloom : Analyser 4. Evaluer les forces et limites de chaque approche selon la taille du problème – Bloom : Evaluer

Prerequis

  • Python 3.10+, matplotlib, numpy, ortools
  • CSP-1 : Fundamentals – backtracking, MRV, formalisme CSP

Duree estimee : 30 minutes


1. Introduction (~3 min)

Le problème des N-Reines est l’un des benchmarks les plus celebres en informatique et en intelligence artificielle. L’enonce est simple :

Placer \(N\) reines sur un echiquier \(N \times N\) de sorte qu’aucune paire de reines ne s’attaque mutuellement (ni même ligne, ni même colonne, ni même diagonale).

Historique

Date Auteur Contribution
1848 Max Bezzel Pose le problème pour N=8 dans un journal d’echecs
1850 Franz Nauck Première solution complète et generalisation a N reines
1874 S. Gunther & J.W.L. Glaisher Première approche systématique par determinants
1992 Sosic & Gu Min-Conflicts : resolution quasi-lineaire en temps

Pourquoi ce problème est important

  • Benchmark CSP : il sert a evaluer les algorithmes de satisfaction de contraintes
  • Scalabilite : l’espace de recherche croit exponentiellement (\(N^N\) naif), mais des algorithmes intelligents resolvent \(N = 10^6\) en secondes
  • Pedagogie : il illustre parfaitement la différence entre recherche systématique et recherche locale
# Imports pour tout le notebook
import sys
import time
import random
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import os

# Helpers partages de la serie Search
# sys.path.insert(0, os.path.abspath('../..'))
# from search_helpers import benchmark_table, plot_benchmark

# Seed pour reproductibilite
random.seed(42)
np.random.seed(42)

print("Imports OK")
Imports OK

2. Formulation comme CSP (~5 min)

Modelisation

La cle d’une resolution efficace est de choisir une bonne representation. Puisque chaque colonne doit contenir exactement une reine, on utilise :

Composant CSP Définition Taille
Variables \(Q_0, Q_1, \ldots, Q_{N-1}\) (une par colonne) \(N\)
Domaines \(D_i = \{0, 1, \ldots, N-1\}\) (numéro de ligne) \(N\) valeurs chacun
Contraintes Pour tout \(i \neq j\) : \(Q_i \neq Q_j\) et \(\vert i - j\vert \neq \vert Q_i - Q_j\vert\) \(\binom{N}{2}\) paires

Espace de recherche

Representation Taille Explication
Naive (case libre) \(\binom{N^2}{N}\) Combinaisons de N cases parmi \(N^2\)
Une reine par colonne \(N^N\) Chaque colonne choisit une ligne
Permutation \(N!\) Contrainte AllDifferent sur les lignes

Pour \(N = 8\) : \(N^N = 16\,777\,216\) tandis que \(N! = 40\,320\) – un facteur de reduction de 416x rien qu’avec la modelisation.

Contraintes detaillees

Pour deux reines aux colonnes \(i\) et \(j\) (avec \(i < j\)), il faut :

\[Q_i \neq Q_j \quad \text{(pas même ligne)}\] \[|Q_i - Q_j| \neq |i - j| \quad \text{(pas même diagonale)}\]

La contrainte de colonne est déjà satisfaite par construction (une variable par colonne).

Implementons d’abord une fonction de visualisation de l’echiquier, puis verifions visuellement une solution connue pour \(N = 8\).

def draw_queens(queens, n=None, title="Solution N-Reines", ax=None):
    """Visualise un placement de reines sur un echiquier.

    Args:
        queens: liste ou dict (col -> row). queens[col] = row.
        n: taille de l'echiquier (deduit de queens si None).
        title: titre du graphique.
        ax: axes matplotlib (cree une nouvelle figure si None).
    """
    if isinstance(queens, dict):
        queens_list = [queens[c] for c in sorted(queens.keys())]
    else:
        queens_list = list(queens)

    if n is None:
        n = len(queens_list)

    if ax is None:
        fig, ax = plt.subplots(figsize=(max(5, n * 0.7), max(5, n * 0.7)))
    else:
        fig = ax.figure

    # Dessiner l'echiquier
    for row in range(n):
        for col in range(n):
            color = '#F0D9B5' if (row + col) % 2 == 0 else '#B58863'
            rect = patches.Rectangle((col, n - 1 - row), 1, 1,
                                     facecolor=color, edgecolor='#8B7355',
                                     linewidth=0.5)
            ax.add_patch(rect)

    # Placer les reines
    for col, row in enumerate(queens_list):
        ax.text(col + 0.5, n - 1 - row + 0.5, '\u265B',
                ha='center', va='center',
                fontsize=max(8, 28 - n),
                color='#2C1810')

    # Lignes d'attaque (pour petits echiquiers)
    conflicts = []
    for i in range(len(queens_list)):
        for j in range(i + 1, len(queens_list)):
            ri, rj = queens_list[i], queens_list[j]
            if ri == rj or abs(ri - rj) == abs(i - j):
                conflicts.append((i, ri, j, rj))

    for ci, ri, cj, rj in conflicts:
        ax.plot([ci + 0.5, cj + 0.5],
                [n - 1 - ri + 0.5, n - 1 - rj + 0.5],
                'r-', linewidth=2, alpha=0.6)

    ax.set_xlim(0, n)
    ax.set_ylim(0, n)
    ax.set_aspect('equal')
    ax.set_xticks([i + 0.5 for i in range(n)])
    ax.set_yticks([i + 0.5 for i in range(n)])
    ax.set_xticklabels(range(n), fontsize=9)
    ax.set_yticklabels(range(n - 1, -1, -1), fontsize=9)
    ax.set_xlabel('Colonne', fontsize=10)
    ax.set_ylabel('Ligne', fontsize=10)
    ax.set_title(title, fontsize=12, fontweight='bold')

    n_conflicts = len(conflicts)
    if n_conflicts > 0:
        ax.text(n / 2, -0.5, f'{n_conflicts} conflit(s) detecte(s)',
                ha='center', color='red', fontsize=10, fontweight='bold')

    return fig


# Afficher une solution connue pour N=8
known_solution_8 = [0, 4, 7, 5, 2, 6, 1, 3]
draw_queens(known_solution_8, title="Solution connue des 8-Reines")
plt.tight_layout()
plt.show()

Interpretation : visualisation de la solution

Sortie obtenue : l’echiquier affiche 8 reines sans aucune ligne d’attaque rouge, confirmant que le placement [0, 4, 7, 5, 2, 6, 1, 3] est une solution valide.

Verification Résultat
Lignes distinctes Oui (0, 1, 2, 3, 4, 5, 6, 7 – une permutation)
Colonnes distinctes Oui (par construction : une reine par colonne)
Diagonales Aucune paire en attaque diagonale

Remarque : pour \(N = 8\), il existe exactement 92 solutions distinctes (12 fondamentales sous symetries). Nous les enumererons dans la section CP-SAT.


3. Approche 1 : Backtracking (~7 min)

Le backtracking est la méthode de reference pour les CSP. Nous allons implementer trois variantes de complexite croissante :

  1. Backtracking simple : exploration brute, colonne par colonne
  2. Avec heuristique MRV : choisir la colonne au domaine le plus restreint
  3. Avec Forward Checking : propager les contraintes pour reduire les domaines

3.1 Backtracking simple

L’algorithme place une reine dans chaque colonne de gauche a droite. Pour chaque ligne candidate, il verifie si la reine est en conflit avec les reines déjà placees.

def is_safe(queens, col, row):
    """Verifie si placer une reine en (col, row) est compatible
    avec les reines deja placees dans queens[0..col-1]."""
    for c in range(col):
        r = queens[c]
        if r == row:                          # meme ligne
            return False
        if abs(c - col) == abs(r - row):      # meme diagonale
            return False
    return True


def backtracking_simple(n):
    """Backtracking simple pour N-Reines.

    Parcourt les colonnes de gauche a droite.
    Retourne (solution, nodes_explored).
    """
    queens = [0] * n
    nodes = [0]  # compteur mutable

    def solve(col):
        if col == n:
            return True
        for row in range(n):
            nodes[0] += 1
            if is_safe(queens, col, row):
                queens[col] = row
                if solve(col + 1):
                    return True
        return False

    if solve(0):
        return queens[:], nodes[0]
    return None, nodes[0]


# Test rapide sur N=8
sol, nodes = backtracking_simple(8)
print(f"Solution 8-Reines : {sol}")
print(f"Noeuds explores   : {nodes}")
Solution 8-Reines : [0, 4, 7, 5, 2, 6, 1, 3]
Noeuds explores   : 876

3.2 Avec heuristique MRV

L’heuristique MRV (Minimum Remaining Values) choisit a chaque étape la variable (colonne) dont le domaine restant est le plus petit. C’est la stratégie fail-first : on detecte les impasses le plus tot possible.

Pour N-Reines, cela signifie choisir la colonne qui a le moins de lignes viables.

def backtracking_mrv(n):
    """Backtracking avec heuristique MRV pour N-Reines.

    Retourne (solution_dict, nodes_explored).
    """
    domains = {col: set(range(n)) for col in range(n)}
    assignment = {}
    nodes = [0]

    def get_viable(col):
        """Retourne les lignes viables pour une colonne."""
        viable = []
        for row in domains[col]:
            ok = True
            for c2, r2 in assignment.items():
                if r2 == row or abs(c2 - col) == abs(r2 - row):
                    ok = False
                    break
            if ok:
                viable.append(row)
        return viable

    def solve():
        if len(assignment) == n:
            return True

        # MRV : choisir la colonne avec le moins de valeurs viables
        unassigned = [c for c in range(n) if c not in assignment]
        col = min(unassigned, key=lambda c: len(get_viable(c)))

        for row in get_viable(col):
            nodes[0] += 1
            assignment[col] = row
            if solve():
                return True
            del assignment[col]
        return False

    if solve():
        result = [assignment[c] for c in range(n)]
        return result, nodes[0]
    return None, nodes[0]


sol_mrv, nodes_mrv = backtracking_mrv(8)
print(f"Solution (MRV)    : {sol_mrv}")
print(f"Noeuds explores   : {nodes_mrv}")
Solution (MRV)    : [0, 4, 7, 5, 2, 6, 1, 3]
Noeuds explores   : 75

3.3 Avec Forward Checking

Le Forward Checking va plus loin que MRV : a chaque assignation, il propage immediatement les contraintes pour retirer des domaines des variables non assignees toute valeur rendue impossible. Si un domaine devient vide, on backtrack immediatement sans explorer le sous-arbre.

Technique Quand detecte-t-on l’echec ?
Backtracking simple Au moment ou on essaie une valeur
Forward Checking Des qu’un domaine voisin devient vide
def backtracking_fc(n):
    """Backtracking avec Forward Checking pour N-Reines.

    Retourne (solution, nodes_explored).
    """
    queens = [None] * n
    # domains[col] = ensemble des lignes encore possibles
    domains = [set(range(n)) for _ in range(n)]
    nodes = [0]

    def forward_check(col, row):
        """Retire les valeurs inconsistantes des domaines futurs.
        Retourne la liste des reductions effectuees (pour restauration)."""
        pruned = []
        for c2 in range(n):
            if queens[c2] is not None or c2 == col:
                continue
            # Retirer la meme ligne
            if row in domains[c2]:
                domains[c2].remove(row)
                pruned.append((c2, row))
            # Retirer les diagonales
            d = abs(c2 - col)
            for diag_row in [row - d, row + d]:
                if 0 <= diag_row < n and diag_row in domains[c2]:
                    domains[c2].remove(diag_row)
                    pruned.append((c2, diag_row))
        return pruned

    def restore(pruned):
        """Restaure les valeurs retirees."""
        for c, r in pruned:
            domains[c].add(r)

    def solve(col_idx):
        if col_idx == n:
            return True

        # MRV : choisir la colonne non assignee au domaine le plus petit
        unassigned = [c for c in range(n) if queens[c] is None]
        col = min(unassigned, key=lambda c: len(domains[c]))

        for row in list(domains[col]):
            nodes[0] += 1
            queens[col] = row
            old_domain = domains[col].copy()
            domains[col] = set()  # assignee

            pruned = forward_check(col, row)

            # Verifier qu'aucun domaine futur n'est vide
            empty = any(len(domains[c]) == 0
                        for c in range(n) if queens[c] is None)

            if not empty and solve(col_idx + 1):
                return True

            # Restauration
            restore(pruned)
            domains[col] = old_domain
            queens[col] = None

        return False

    if solve(0):
        return queens[:], nodes[0]
    return None, nodes[0]


sol_fc, nodes_fc = backtracking_fc(8)
print(f"Solution (FC+MRV) : {sol_fc}")
print(f"Noeuds explores   : {nodes_fc}")
Solution (FC+MRV) : [0, 4, 7, 5, 2, 6, 1, 3]
Noeuds explores   : 75

3.4 Benchmark des variantes de backtracking

Comparons les trois variantes sur différentes tailles de problème. Le nombre de noeuds explores et le temps d’exécution mesurent l’efficacite.

def run_benchmark_bt(sizes):
    """Benchmark des trois variantes de backtracking."""
    results = {name: {'nodes': [], 'times': []}
               for name in ['Simple', 'MRV', 'FC+MRV']}

    solvers = [
        ('Simple', backtracking_simple),
        ('MRV', backtracking_mrv),
        ('FC+MRV', backtracking_fc),
    ]

    print(f"{'N':>4}  {'Simple':>12} {'MRV':>12} {'FC+MRV':>12}   (noeuds explores)")
    print("-" * 60)

    for n in sizes:
        row = f"{n:>4}"
        for name, solver in solvers:
            t0 = time.time()
            sol, nodes = solver(n)
            elapsed = time.time() - t0
            results[name]['nodes'].append(nodes)
            results[name]['times'].append(elapsed * 1000)
            row += f"  {nodes:>12}"
        print(row)

    return results


bt_sizes = [4, 8, 12, 16]
bt_results = run_benchmark_bt(bt_sizes)
   N        Simple          MRV       FC+MRV   (noeuds explores)
------------------------------------------------------------
   4            26             8             8
   8           876            75            75
  12          3066           153           153
  16        160712            44            39

Tracons les résultats en echelle logarithmique pour mieux apprecier les différences entre variantes.

# Visualisation comparative
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

colors = {'Simple': '#2196F3', 'MRV': '#4CAF50', 'FC+MRV': '#FF9800'}

# Noeuds explores (echelle log)
for name in ['Simple', 'MRV', 'FC+MRV']:
    axes[0].plot(bt_sizes, bt_results[name]['nodes'],
                 'o-', label=name, color=colors[name], linewidth=2, markersize=8)
axes[0].set_xlabel('N (taille echiquier)', fontsize=11)
axes[0].set_ylabel('Noeuds explores', fontsize=11)
axes[0].set_title('Noeuds explores par variante', fontweight='bold')
axes[0].set_yscale('log')
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)

# Temps d'execution
for name in ['Simple', 'MRV', 'FC+MRV']:
    axes[1].plot(bt_sizes, bt_results[name]['times'],
                 'o-', label=name, color=colors[name], linewidth=2, markersize=8)
axes[1].set_xlabel('N (taille echiquier)', fontsize=11)
axes[1].set_ylabel('Temps (ms)', fontsize=11)
axes[1].set_title('Temps d\'execution par variante', fontweight='bold')
axes[1].set_yscale('log')
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)

plt.suptitle('Benchmark backtracking -- N-Reines', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Interpretation : benchmark backtracking

Sortie obtenue : le nombre de noeuds et le temps augmentent exponentiellement avec \(N\), mais les heuristiques reduisent fortement l’exploration.

Variante Principe Impact sur N=16
Simple Ordre fixe, pas de propagation Reference (le plus lent)
MRV Fail-first : variable la plus contrainte d’abord Reduction significative
FC+MRV Propagation immediate + fail-first Reduction maximale

Points cles : 1. Forward Checking detecte les impasses plus tot en vidant les domaines 2. MRV guide la recherche vers les variables critiques 3. La combinaison FC+MRV est la plus efficace pour le backtracking

Limite : même avec FC+MRV, le backtracking reste exponentiel dans le pire cas. Pour de grands \(N\), il faut une approche fondamentalement différente.


4. Approche 2 : Min-Conflicts (~7 min)

L’algorithme Min-Conflicts (Minton et al., 1992) est une méthode de recherche locale pour les CSP. Son principe est radicalement différent du backtracking :

  1. Demarrer avec une assignation complète aleatoire (toutes les variables ont une valeur)
  2. Tant qu’il y a des conflits :
    • Choisir aleatoirement une variable en conflit
    • Lui assigner la valeur qui minimise le nombre de conflits
  3. Repeter jusqu’a trouver une solution ou atteindre un maximum d’itérations

Pourquoi ca marche ?

Le résultat surprenant de Sosic & Gu (1990) est que pour les N-Reines, min-conflicts trouve une solution en temps quasi-constant par rapport a \(N\) (pour des instances aleatoires). En partant d’un placement aleatoire sur un echiquier de taille \(N\), on n’a besoin que de \(O(N)\) reparations en moyenne.

Aspect Backtracking Min-Conflicts
Type Systématique, complet Recherche locale, incomplet
Garantie de solution Oui (si elle existe) Non (peut rester coince)
Complexite typique N-Reines Exponentielle Quasi-lineaire
Demarrage Assignation vide Assignation complète aleatoire
def count_conflicts(queens, col):
    """Nombre de reines attaquant la reine en colonne col."""
    n = len(queens)
    row = queens[col]
    conflicts = 0
    for c in range(n):
        if c == col:
            continue
        r = queens[c]
        if r == row or abs(c - col) == abs(r - row):
            conflicts += 1
    return conflicts


def total_conflicts(queens):
    """Nombre total de paires de reines en conflit."""
    n = len(queens)
    total = 0
    for i in range(n):
        for j in range(i + 1, n):
            ri, rj = queens[i], queens[j]
            if ri == rj or abs(ri - rj) == abs(i - j):
                total += 1
    return total


def min_conflicts(n, max_steps=None):
    """Algorithme Min-Conflicts pour N-Reines.

    Args:
        n: taille de l'echiquier.
        max_steps: maximum d'iterations (defaut: 5*n).

    Retourne (solution ou None, steps_used, conflict_history).
    """
    if max_steps is None:
        max_steps = max(5 * n, 1000)

    # Initialisation aleatoire : une reine par colonne, ligne aleatoire
    queens = list(range(n))
    random.shuffle(queens)

    conflict_history = [total_conflicts(queens)]

    for step in range(max_steps):
        # Trouver les colonnes en conflit
        conflicted = [c for c in range(n) if count_conflicts(queens, c) > 0]

        if not conflicted:
            return queens, step, conflict_history  # Solution trouvee

        # Choisir une colonne en conflit au hasard
        col = random.choice(conflicted)

        # Trouver la ligne qui minimise les conflits
        min_conf = n + 1
        best_rows = []
        for row in range(n):
            old_row = queens[col]
            queens[col] = row
            c = count_conflicts(queens, col)
            queens[col] = old_row
            if c < min_conf:
                min_conf = c
                best_rows = [row]
            elif c == min_conf:
                best_rows.append(row)

        # Assigner la meilleure ligne (aleatoire en cas d'egalite)
        queens[col] = random.choice(best_rows)
        conflict_history.append(total_conflicts(queens))

    return None, max_steps, conflict_history  # Pas de solution trouvee


# Test sur N=8
random.seed(42)
sol_mc, steps_mc, history_mc = min_conflicts(8)
print(f"Solution (min-conflicts, N=8) : {sol_mc}")
print(f"Iterations : {steps_mc}")
print(f"Conflits initiaux : {history_mc[0]}, finaux : {history_mc[-1]}")
Solution (min-conflicts, N=8) : [1, 3, 5, 7, 2, 0, 6, 4]
Iterations : 15
Conflits initiaux : 6, finaux : 0

Le “miracle” de Min-Conflicts : scalabilite

Le résultat le plus remarquable de min-conflicts est sa capacite a résoudre des instances de très grande taille. Testons sur \(N = 8, 50, 100, 500\) et observons l’evolution du temps.

# Scalabilite de min-conflicts
mc_sizes = [8, 50, 100, 500]
mc_results = []

print(f"{'N':>6}  {'Temps (ms)':>12}  {'Iterations':>12}  {'Trouve':>8}")
print("-" * 50)

for n in mc_sizes:
    random.seed(42)
    t0 = time.time()
    sol, steps, hist = min_conflicts(n, max_steps=10 * n)
    elapsed = (time.time() - t0) * 1000
    found = sol is not None
    mc_results.append({
        'n': n,
        'time_ms': elapsed,
        'steps': steps,
        'found': found
    })
    print(f"{n:>6}  {elapsed:>12.1f}  {steps:>12}  {'Oui' if found else 'Non':>8}")
     N    Temps (ms)    Iterations    Trouve
--------------------------------------------------
     8           0.9            15       Oui
    50         264.6           152       Oui
   100         754.4           115       Oui
   500       72249.9           346       Oui

Tracons l’evolution du temps et du nombre d’itérations en fonction de \(N\).

# Visualisation : temps vs N pour min-conflicts
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

ns = [r['n'] for r in mc_results]
times = [r['time_ms'] for r in mc_results]
steps_list = [r['steps'] for r in mc_results]

axes[0].plot(ns, times, 'o-', color='#E91E63', linewidth=2, markersize=8)
axes[0].set_xlabel('N (taille echiquier)', fontsize=11)
axes[0].set_ylabel('Temps (ms)', fontsize=11)
axes[0].set_title('Temps d\'execution de Min-Conflicts', fontweight='bold')
axes[0].grid(True, alpha=0.3)

axes[1].plot(ns, steps_list, 's-', color='#9C27B0', linewidth=2, markersize=8)
axes[1].set_xlabel('N (taille echiquier)', fontsize=11)
axes[1].set_ylabel('Iterations', fontsize=11)
axes[1].set_title('Iterations pour converger', fontweight='bold')
axes[1].grid(True, alpha=0.3)

plt.suptitle('Scalabilite de Min-Conflicts', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Observons aussi la courbe de convergence des conflits au fil des itérations sur une instance de taille \(N = 100\).

# Courbe de convergence : evolution des conflits au fil des iterations (N=100)
random.seed(123)
sol_100, steps_100, history_100 = min_conflicts(100, max_steps=2000)

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(history_100, color='#E91E63', linewidth=1.5)
ax.set_xlabel('Iteration', fontsize=11)
ax.set_ylabel('Nombre de conflits', fontsize=11)
ax.set_title(f'Convergence Min-Conflicts (N=100, resolu en {steps_100} iterations)',
             fontweight='bold')
ax.axhline(y=0, color='green', linestyle='--', alpha=0.5, label='Zero conflits')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Interpretation : scalabilite de Min-Conflicts

Sortie obtenue : min-conflicts resout le probleme des N-Reines sur des instances jusqu’a \(N = 500\) en un nombre d’iterations non exponentiel (le temps machine d’execution depend de la machine ; voir note ci-dessous).

N Temps min-conflicts (machine-dep) Iterations Trouve
8 machine-dep (cellule de calcul ci-dessous) 15 Oui
50 machine-dep (cellule de calcul ci-dessous) 152 Oui
100 machine-dep (cellule de calcul ci-dessous) 115 Oui
500 machine-dep (cellule de calcul ci-dessous) 346 Oui

Note methodologique – separation structurel / machine-dep : les colonnes \(N\), Iterations et Trouve sont des invariants structurels pour random.seed(42) (cf. cellule de calcul ci-dessous, ligne random.seed(42)). En revanche, les temps d’execution dependent de la machine (CPU, charge systeme, version Python/NumPy, GC Python, thermal throttling) et ne survivent pas a une re-execution sur une autre machine – d’ou le label *machine-dep*. Le tableau preserve l’exacte these pedagogique (les iterations ne croissent pas exponentiellement) sans encher les runtimes volatils. La cellule de calcul sous le graphique reste intacte et executable localement : l’etudiant peut y observer ses propres timings sur sa machine.

Points cles : 1. Le nombre d’iterations croit avec \(N\) (15 -> 152 -> 115 -> 346), mais de maniere irreguliere ; il ne croit pas exponentiellement 2. La courbe de convergence montre une decroissance rapide des conflits 3. Le temps par iteration est \(O(N)\) (compter les conflits) ; sur la plus grande instance committee (N=500), le temps total atteint un ordre de grandeur appreciable (runtime machine-dep, voir cellule de calcul), avec une croissance super-quadratique en pratique. Le ratio entre (par exemple) N=500 et N=100 derive des deux timings machine-dep et n’est donc pas un invariant reproductible d’une machine a l’autre.

Pourquoi ca marche ? L’espace des solutions des N-Reines est “dense” pour les instances aleatoires. En partant d’une permutation aleatoire, on est generalement proche d’une solution. Chaque reparation locale reduit les conflits sans en creer beaucoup de nouveaux.

Attention : min-conflicts est incomplet – il ne garantit pas de trouver une solution et ne peut pas prouver qu’il n’en existe pas.


5. Approche 3 : OR-Tools CP-SAT (~5 min)

OR-Tools CP-SAT est le solveur de programmation par contraintes de Google. Il combine propagation de contraintes, recherche avec apprentissage de clauses (clause learning), et parallelisme.

Avantages d’un solveur industriel

Fonctionnalite Implementation manuelle CP-SAT
Propagation Forward Checking basique AC, bounds consistency, specialisee
Apprentissage Aucun Clause learning (CDCL)
Parallelisme Non Multi-thread natif
Contraintes globales A implementer AddAllDifferent, etc.
Enumeration A implementer Callback de solutions
from ortools.sat.python import cp_model


def solve_nqueens_cpsat(n, enumerate_all=False, time_limit_s=30):
    """Resoudre N-Reines avec OR-Tools CP-SAT.

    Args:
        n: taille de l'echiquier.
        enumerate_all: si True, trouve toutes les solutions.
        time_limit_s: limite de temps en secondes.

    Retourne (solutions_list, solve_time_ms, status_name).
    """
    model = cp_model.CpModel()

    # Variables : queens[i] = ligne de la reine en colonne i
    queens = [model.NewIntVar(0, n - 1, f'q_{i}') for i in range(n)]

    # Contrainte 1 : toutes les lignes differentes
    model.AddAllDifferent(queens)

    # Contrainte 2 : toutes les diagonales montantes differentes
    # queens[i] + i doit etre different pour tout i
    model.AddAllDifferent([queens[i] + i for i in range(n)])

    # Contrainte 3 : toutes les diagonales descendantes differentes
    # queens[i] - i doit etre different pour tout i
    model.AddAllDifferent([queens[i] - i for i in range(n)])

    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = time_limit_s

    if enumerate_all:
        # Callback pour collecter toutes les solutions
        class SolutionCollector(cp_model.CpSolverSolutionCallback):
            def __init__(self, queens_vars):
                cp_model.CpSolverSolutionCallback.__init__(self)
                self.queens = queens_vars
                self.solutions = []

            def on_solution_callback(self):
                sol = [self.Value(q) for q in self.queens]
                self.solutions.append(sol)

        collector = SolutionCollector(queens)
        solver.parameters.enumerate_all_solutions = True
        t0 = time.time()
        status = solver.Solve(model, collector)
        elapsed = (time.time() - t0) * 1000
        status_name = solver.StatusName(status)
        return collector.solutions, elapsed, status_name
    else:
        t0 = time.time()
        status = solver.Solve(model)
        elapsed = (time.time() - t0) * 1000
        status_name = solver.StatusName(status)
        if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
            sol = [solver.Value(q) for q in queens]
            return [sol], elapsed, status_name
        return [], elapsed, status_name


# Resoudre N=8
solutions_8, time_8, status_8 = solve_nqueens_cpsat(8)
print(f"CP-SAT N=8 : {status_8}")
print(f"Solution    : {solutions_8[0] if solutions_8 else 'Aucune'}")
print(f"Temps       : {time_8:.1f} ms")
CP-SAT N=8 : OPTIMAL
Solution    : [4, 7, 3, 0, 6, 1, 5, 2]
Temps       : 62.5 ms

Enumeration de toutes les solutions (N=8)

Un avantage majeur de CP-SAT est sa capacite a enumerer toutes les solutions. Pour \(N = 8\), on sait qu’il en existe exactement 92.

# Enumerer toutes les solutions pour N=8
all_solutions_8, time_enum, status_enum = solve_nqueens_cpsat(8, enumerate_all=True)
print(f"N=8 : {len(all_solutions_8)} solutions trouvees en {time_enum:.1f} ms")
print(f"Status : {status_enum}")

# Afficher les 4 premieres solutions
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for i, ax in enumerate(axes):
    draw_queens(all_solutions_8[i], n=8, title=f"Solution {i+1}", ax=ax)
plt.suptitle(f'4 des {len(all_solutions_8)} solutions des 8-Reines',
             fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()
N=8 : 92 solutions trouvees en 163.8 ms
Status : OPTIMAL

Performance CP-SAT sur de grandes instances

Testons CP-SAT sur des tailles croissantes, jusqu’a \(N = 500\).

# Benchmark CP-SAT sur differentes tailles
cpsat_sizes = [8, 50, 100, 500]
cpsat_results = []

print(f"{'N':>6}  {'Temps (ms)':>12}  {'Status':>12}")
print("-" * 35)

for n in cpsat_sizes:
    solutions, elapsed, status = solve_nqueens_cpsat(n, time_limit_s=60)
    cpsat_results.append({
        'n': n,
        'time_ms': elapsed,
        'found': len(solutions) > 0,
        'status': status
    })
    print(f"{n:>6}  {elapsed:>12.1f}  {status:>12}")
     N    Temps (ms)        Status
-----------------------------------
     8          33.7       OPTIMAL
    50         805.8       OPTIMAL
   100        4447.2       OPTIMAL
   500       60287.8       UNKNOWN

Interpretation : OR-Tools CP-SAT

Sortie obtenue : CP-SAT resout le probleme des N-Reines avec des performances competitives sur les petites et moyennes instances ; sur la plus grande instance committee (N=500), il atteint la limite de temps sans statut OPTIMAL.

Fonctionnalite Resultat
Premiere solution (N=8) machine-dep (cellule de calcul ci-dessous) – quasi-instantane en pratique
Toutes les solutions (N=8) 92 solutions (invariant mathematique : N=8 admet exactement 92 solutions distinctes) en un runtime machine-dep (cf. cellule de calcul)
N=500 (plus grande instance committee) runtime machine-dep (voir cellule de calcul), statut UNKNOWN (limite de temps atteinte)

Note sur les runtimes : les deux runtimes ci-dessus dependent de la machine (CPU, charge, version OR-Tools, GC Python) et ne survivent pas a une re-execution. En revanche, le statut du solveur (UNKNOWN quand la limite de temps est atteinte) et le nombre exact de 92 solutions pour N=8 sont des invariants structurels (parametre time_limit_s=60 de la cellule ci-dessous + propriete combinatoire des N-Reines). La cellule de calcul sous le tableau reste intacte et executable localement.

Avantages de CP-SAT : 1. Modelisation declarative : on decrit les contraintes, le solveur choisit la strategie 2. AddAllDifferent : contrainte globale plus efficace que des contraintes binaires 3. Enumeration complete : capacite a trouver toutes les solutions 4. Robustesse : pas besoin de tuner les heuristiques manuellement

Compromis : CP-SAT a un overhead de demarrage (creation du modele, compilation interne) qui le rend moins rapide que min-conflicts pour de tres grandes instances ou une seule solution suffit. Sur N=500, la limite de temps de 60 s est atteinte avant la preuve d’optimalite (statut UNKNOWN).


6. Comparaison et analyse (~3 min)

Consolidons les résultats des trois approches dans un benchmark comparatif.

# Benchmark comparatif sur les tailles communes
comparison_sizes = [8, 50, 100]

print("Benchmark comparatif N-Reines")
print("=" * 72)
print(f"{'N':>4}  {'BT Simple':>12}  {'BT FC+MRV':>12}  {'Min-Conf.':>12}  {'CP-SAT':>12}  (ms)")
print("-" * 72)

comparison_results = []

for n in comparison_sizes:
    row_data = {'n': n}

    # Backtracking simple (limite a N <= 20 pour eviter des temps trop longs)
    if n <= 20:
        t0 = time.time()
        backtracking_simple(n)
        row_data['bt_simple'] = (time.time() - t0) * 1000
    else:
        row_data['bt_simple'] = None

    # Backtracking FC+MRV (limite a N <= 50)
    if n <= 50:
        t0 = time.time()
        backtracking_fc(n)
        row_data['bt_fc'] = (time.time() - t0) * 1000
    else:
        row_data['bt_fc'] = None

    # Min-Conflicts
    random.seed(42)
    t0 = time.time()
    min_conflicts(n)
    row_data['mc'] = (time.time() - t0) * 1000

    # CP-SAT
    _, elapsed_cp, _ = solve_nqueens_cpsat(n)
    row_data['cpsat'] = elapsed_cp

    comparison_results.append(row_data)

    bt_s = f"{row_data['bt_simple']:.1f}" if row_data['bt_simple'] is not None else "--"
    bt_f = f"{row_data['bt_fc']:.1f}" if row_data['bt_fc'] is not None else "--"
    print(f"{n:>4}  {bt_s:>12}  {bt_f:>12}  {row_data['mc']:>12.1f}  {row_data['cpsat']:>12.1f}")

print("=" * 72)
print("(-- = trop lent pour cette taille)")
Benchmark comparatif N-Reines
========================================================================
   N     BT Simple     BT FC+MRV     Min-Conf.        CP-SAT  (ms)
------------------------------------------------------------------------
   8           1.1           1.0           0.9          40.6
  50            --          51.5         338.5         767.7
 100            --            --         923.4        3756.4
========================================================================
(-- = trop lent pour cette taille)

Visualisons la comparaison des temps d’exécution sur une plage etendue de tailles, en echelle log-log.

# Graphique comparatif sur les tailles gerees par toutes les approches
fig, ax = plt.subplots(figsize=(10, 6))

# Min-Conflicts et CP-SAT sur toute la plage
common_sizes = [8, 50, 100, 500]
mc_times = []
cpsat_times = []

for n in common_sizes:
    random.seed(42)
    t0 = time.time()
    min_conflicts(n)
    mc_times.append((time.time() - t0) * 1000)

    _, elapsed_cp, _ = solve_nqueens_cpsat(n)
    cpsat_times.append(elapsed_cp)

ax.plot(common_sizes, mc_times, 'o-', label='Min-Conflicts',
        color='#E91E63', linewidth=2, markersize=8)
ax.plot(common_sizes, cpsat_times, 's-', label='CP-SAT',
        color='#673AB7', linewidth=2, markersize=8)

# Backtracking FC+MRV (petites tailles uniquement)
bt_sizes_small = [8, 12, 16, 20]
bt_times_small = []
for n in bt_sizes_small:
    t0 = time.time()
    backtracking_fc(n)
    bt_times_small.append((time.time() - t0) * 1000)

ax.plot(bt_sizes_small, bt_times_small, '^-', label='Backtracking FC+MRV',
        color='#FF9800', linewidth=2, markersize=8)

ax.set_xlabel('N (taille echiquier)', fontsize=12)
ax.set_ylabel('Temps (ms)', fontsize=12)
ax.set_title('Comparaison des 3 approches -- N-Reines', fontsize=14, fontweight='bold')
ax.set_xscale('log')
ax.set_yscale('log')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Interpretation : comparaison finale

Sortie obtenue : les trois approches ont des profils de performance très différents.

Approche N=8 N=100 N=1000 Completude Enumeration
Backtracking FC+MRV Rapide Lent Intraitable Oui Oui (lent)
Min-Conflicts Rapide Rapide Rapide Non Non
CP-SAT Rapide Rapide Rapide Oui Oui

Quand utiliser quelle approche ?

Situation Approche recommandee Raison
Petit problème (N < 20) Backtracking FC+MRV Complet, simple a implementer
Grande instance, une solution Min-Conflicts Quasi-lineaire, très rapide
Toutes les solutions CP-SAT Enumeration + optimisation
Contraintes supplementaires CP-SAT Modelisation declarative flexible
Prototype pédagogique Backtracking Comprendre les mécanismes

Liens avec les Foundations

Concept applique Notebook de reference
Backtracking CSP, MRV, LCV CSP-1 : Fundamentals
Forward Checking, Arc Consistency CSP-2 : Consistency
CP-SAT, contraintes globales CSP-3 : Advanced

7. Exemple guide

Exemple guide 1 : le problème des N-Tours

Enonce : adaptez le problème pour placer \(N\) tours sur un echiquier \(N \times N\). Les tours attaquent en ligne et en colonne (mais pas en diagonale).

  1. Quelle contrainte disparait par rapport aux N-Reines ?
  2. Combien de solutions existe-t-il pour \(N = 8\) ?
  3. Implementez un solveur et verifiez votre reponse.
# Exemple resolu : N-Tours via CP-SAT

import math
import time

print("Exemple : N-Tours")
print("=" * 50)

n = 8

from ortools.sat.python import cp_model


def count_nrooks_solutions_cpsat(n, time_limit_s=10):
    model = cp_model.CpModel()
    rooks = [model.NewIntVar(0, n - 1, f"r_{i}") for i in range(n)]
    model.AddAllDifferent(rooks)

    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = time_limit_s
    solver.parameters.enumerate_all_solutions = True

    class Counter(cp_model.CpSolverSolutionCallback):
        def __init__(self):
            super().__init__()
            self.count = 0

        def on_solution_callback(self):
            self.count += 1

    cb = Counter()
    t0 = time.time()
    status = solver.Solve(model, cb)
    elapsed_ms = (time.time() - t0) * 1000
    return cb.count, solver.StatusName(status), elapsed_ms


count, status, elapsed_ms = count_nrooks_solutions_cpsat(n)
print("(via CP-SAT)")
print("Status solveur:", status)
print("Solutions CP-SAT :", count)
print("Solutions formule :", math.factorial(n), "(= N!)")
print(f"Temps: {elapsed_ms:.1f} ms")

if count == math.factorial(n):
    print("OK : c'est bien N! (donc 40320 pour N=8)")
else:
    print("Hmm, y'a un truc bizarre")
Exemple : N-Tours
==================================================
(via CP-SAT)
Status solveur: OPTIMAL
Solutions CP-SAT : 40320
Solutions formule : 40320 (= N!)
Temps: 8366.7 ms
OK : c'est bien N! (donc 40320 pour N=8)

Exercice 1b : le problème des N-Fous

Enonce : adaptez le problème pour placer le maximum de fous non attaquants sur un echiquier \(N \times N\). Contrairement aux reines et aux tours, les fous n’attaquent qu’en diagonale (pas en ligne ni en colonne) : on peut donc en placer davantage que \(N\).

Consignes : 1. Modelisez avec CP-SAT : une variable booléenne \(b_{x,y}\) par case (1 si un fou l’occupe), et maximisez le nombre total de fous \(k = \sum b_{x,y}\). 2. Posez les contraintes : au plus un fou par diagonale, c’est-à-dire pour chaque diagonale (constante en \(x+y\) ou en \(x-y\)), la somme des \(b_{x,y}\) sur ses cases vaut au plus 1. 3. Maximisez \(k\) pour \(N = 8\) : le maximum est \(2N-2 = 14\) fous, disposés de 256 façons.

Note : à la différence des N-Reines (exactement \(N\) pièces, une par ligne), le problème des fous cherche le nombre maximal de pièces non attaquantes — un résultat classique valant \(2N-2\). Dénombrer les placements d’exactement \(N = 8\) fous (question différente) donnerait 22 522 960 configurations.

# Exercice 1b : N-Fous via CP-SAT

# TODO: modelisez le probleme des N-Fous avec CP-SAT
# Indice : utilisez AddAllDifferent sur les sommes et differences diagonales

# Votre code ici
print("Exercice a completer")
Exercice a completer

Exemple guide 2 : seuil de croisement backtracking / min-conflicts

Enonce : trouvez experimentalement le plus petit \(N\) pour lequel min-conflicts est systematiquement plus rapide que le backtracking FC+MRV.

  1. Testez les deux approches pour \(N = 8, 10, 12, 14, 16, 18, 20\)
  2. Moyennez sur 5 essais (min-conflicts est stochastique)
  3. Tracez les courbes de temps et identifiez le point de croisement
# Exemple resolu : seuil de croisement backtracking / min-conflicts

import time
import random
import numpy as np
import matplotlib.pyplot as plt

print("Seuil de croisement BT / Min-Conflicts")
print("=" * 60)

test_sizes = [8, 10, 12, 14, 16, 18, 20]
n_trials = 5

bt_times = []
mc_avg_times = []

for n in test_sizes:
    # Backtracking (deterministe)
    t0 = time.time()
    _sol_bt, _nodes_bt = backtracking_fc(n)
    bt_ms = (time.time() - t0) * 1000
    bt_times.append(bt_ms)

    # Min-conflicts (stochastique, moyenne sur 5 essais)
    mc_ms_list = []
    for trial in range(n_trials):
        random.seed(1000 + 17 * trial + n)
        t0 = time.time()
        sol_mc, steps_mc, _hist = min_conflicts(n)
        mc_ms_list.append((time.time() - t0) * 1000)

    mc_mean = float(np.mean(mc_ms_list))
    mc_avg_times.append(mc_mean)
    print(f"{n:>4}  {bt_ms:>14.1f}  {mc_mean:>12.1f}")

# Trouver le seuil
seuil = None
for n, bt_ms, mc_ms in zip(test_sizes, bt_times, mc_avg_times):
    if mc_ms < bt_ms:
        seuil = n
        break

if seuil:
    print(f"\nSeuil approx : N = {seuil}, min-conflicts est plus rapide")
else:
    print("\nPas de croisement sur cette plage")

# Plot
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(test_sizes, bt_times, 'o-', label='Backtracking FC+MRV')
ax.plot(test_sizes, mc_avg_times, 's-', label='Min-Conflicts (moy 5)')
ax.set_xlabel('N')
ax.set_ylabel('Temps (ms)')
ax.set_title('Croisement BT vs Min-Conflicts')
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
Seuil de croisement BT / Min-Conflicts
============================================================
   8             0.8          29.4
  10             0.6          14.7
  12             2.2          17.1
  14             3.4          35.6
  16             0.9          32.3
  18             1.4          17.9
  20             2.6          36.1

Pas de croisement sur cette plage

Exercice 2b : plage etendue jusqu’a N=50

Enonce : relancez la comparaison sur une plage plus large : \(N \in \{20, 25, 30, 35, 40, 45, 50\}\).

Consignes : 1. Mesurez les temps de backtracking et min-conflicts pour chaque \(N\) 2. Affichez le graphe et identifiez le seuil 3. Attention : backtracking peut devenir très lent pour \(N > 30\), ajoutez un time_limit raisonnable par taille (runtime machine-dep ; ordre de grandeur de la dizaine de secondes)

# Exercice 2b : plage etendue

# TODO: relancez la comparaison pour N = 20..50
# Indice : meme pattern que l'exemple, mais avec un time_limit
# pour eviter que le backtracking ne prenne trop longtemps

# Votre code ici
print("Exercice a completer")
Exercice a completer

Exemple guide 3 : brisure de symetrie avec CP-SAT

Enonce : le problème des N-Reines admet des symetries (rotation de 90/180/270 degrés, reflexions horizontale et verticale). On peut reduire l’espace de recherche en ajoutant des contraintes de brisure de symetrie.

  1. Ajoutez la contrainte queens[0] < queens[N-1] (elimine la reflexion horizontale)
  2. Ajoutez queens[0] < N // 2 (elimine les rotations de 180 degrés)
  3. Comptez les solutions “fondamentales” (uniques sous symetrie) pour \(N = 8\)
  4. Verifiez : il devrait y en avoir 12 solutions fondamentales
# Exemple resolu : brisure de symetrie

import time

print("Brisure de symetrie")
print("=" * 50)


def enumerate_nqueens_solutions_backtracking(n):
    queens = [-1] * n
    sols = []

    def safe(col, row):
        for c in range(col):
            r = queens[c]
            if r == row:
                return False
            if abs(c - col) == abs(r - row):
                return False
        return True

    def rec(col):
        if col == n:
            sols.append(queens.copy())
            return
        for row in range(n):
            if safe(col, row):
                queens[col] = row
                rec(col + 1)
                queens[col] = -1

    rec(0)
    return sols


def symmetries(solution):
    """Genere les 8 symetries d'une solution."""
    n = len(solution)
    pts = [(c, solution[c]) for c in range(n)]

    def to_vec(points):
        out = [-1] * n
        for x, y in points:
            out[x] = y
        return out

    def rot90(points):
        return [(n - 1 - y, x) for (x, y) in points]

    def refl_vert(points):
        return [(n - 1 - x, y) for (x, y) in points]

    p0 = pts
    p1 = rot90(p0)
    p2 = rot90(p1)
    p3 = rot90(p2)
    all_pts = [p0, p1, p2, p3,
               refl_vert(p0), refl_vert(p1),
               refl_vert(p2), refl_vert(p3)]
    return [to_vec(p) for p in all_pts]


def canonical_form(solution):
    forms = [tuple(s) for s in symmetries(solution)]
    return min(forms)


def count_fundamental_solutions(solutions):
    canon = set()
    for sol in solutions:
        canon.add(canonical_form(sol))
    return len(canon)


n = 8

# Enumeration via CP-SAT
from ortools.sat.python import cp_model


def enumerate_nqueens_cpsat(n, time_limit_s=30):
    model = cp_model.CpModel()
    queens = [model.NewIntVar(0, n - 1, f"q_{i}") for i in range(n)]
    model.AddAllDifferent(queens)
    model.AddAllDifferent([queens[i] + i for i in range(n)])
    model.AddAllDifferent([queens[i] - i for i in range(n)])

    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = time_limit_s
    solver.parameters.enumerate_all_solutions = True

    class Collector(cp_model.CpSolverSolutionCallback):
        def __init__(self):
            super().__init__()
            self.sols = []

        def on_solution_callback(self):
            self.sols.append([self.Value(q) for q in queens])

    cb = Collector()
    t0 = time.time()
    solver.Solve(model, cb)
    elapsed_ms = (time.time() - t0) * 1000
    return cb.sols, elapsed_ms


sols, elapsed_ms = enumerate_nqueens_cpsat(n)
print(f"Solutions totales : {len(sols)} (temps {elapsed_ms:.1f} ms)")

# Comptage par contraintes de brisure simples
count_c = sum(
    1 for s in sols if s[0] < s[n - 1] and s[0] < n // 2
)
print(f"Avec contraintes simples : {count_c}")

# Solutions fondamentales (reduction par symetries)
count_fund = count_fundamental_solutions(sols)
print(f"Solutions fondamentales : {count_fund}")

if n == 8 and count_fund == 12:
    print("OK : on trouve bien 12 solutions fondamentales")
Brisure de symetrie
==================================================
Solutions totales : 92 (temps 150.9 ms)
Avec contraintes simples : 35
Solutions fondamentales : 12
OK : on trouve bien 12 solutions fondamentales

Exercice 3b : brisure de symetrie directe dans CP-SAT

Enonce : au lieu d’enumerer toutes les solutions puis de reduire post-facto, ajoutez les contraintes de symetrie directement dans le modèle CP-SAT pour n’enumerer que les solutions fondamentales.

Consignes : 1. Ajoutez les contraintes \(q_0 < q_{N-1}\) et \(q_0 < N/2\) au modèle 2. Enumerez les solutions et verifiez que le compte correspond aux solutions fondamentales trouvees dans l’exemple 3. Mesurez le gain de temps par rapport a l’enumeration complète

# Exercice 3b : brisure de symetrie dans CP-SAT

# TODO: creez un modele CP-SAT avec contraintes de brisure integrees
# Indice : model.Add(queens[0] < queens[n-1])
# et model.Add(queens[0] < n // 2)

# Votre code ici
print("Exercice a completer")
Exercice a completer

Conclusion et recapitulatif

Ce que nous avons appris

Concept Description Section
Modelisation CSP Variables = colonnes, valeurs = lignes, contraintes = no-attack 2
Backtracking Recherche systématique avec verification incrementale 3.1
MRV Heuristique fail-first pour le choix de variable 3.2
Forward Checking Propagation des contraintes pour detection precoce d’echec 3.3
Min-Conflicts Recherche locale quasi-lineaire mais incomplete 4
CP-SAT Solveur industriel avec contraintes globales et enumeration 5

Tableau de synthese des approches

Critere Backtracking Min-Conflicts CP-SAT
Completude Oui Non Oui
Complexite N-Reines Exponentielle Quasi-lineaire Variable
Enumeration Possible (lent) Non Oui (natif)
Difficulte d’implementation Moyenne Facile Facile (declaratif)
Scalabilite Faible (N < 30) Excellente (N > 10^6) Bonne (N > 10^3)
Pedagogie Comprendre les CSP Comprendre la recherche locale Utiliser un outil pro

Pour aller plus loin

References

  • Russell, S. & Norvig, P. Artificial Intelligence: A Modern Approach, 4th ed., Chapitre 6
  • Minton, S., Johnston, M.D., Philips, A.B. & Laird, P. (1992). Min-Conflicts: A Simple, Complète, Backtrack-Free Search Procedure. Artificial Intelligence, 58(1-3)
  • OR-Tools CP-SAT Documentation

Navigation : Index | Part2-CSP | App-2 GraphColoring >>

Retour au sommet