Sudoku-Python-Genetic : Algorithme Génétique (Python)

Navigation : << Python DancingLinks | Index | Python SimulatedAnnealing >>

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Implementer un algorithme génétique avec PyGAD 2. Encoder un problème de contraintes en chromosome 3. Comprendre pourquoi le Sudoku est difficile pour les algorithmes génétiques 4. Comparer deux stratégies d’encodage : cellules vs permutations

Duree estimee : ~10 min | Prerequis : Sudoku-00 Environment | Lien : Voir Search-5 GeneticAlgorithms


Ce notebook implemente un solveur de Sudoku utilisant un algorithme génétique avec PyGAD. C’est l’equivalent Python du notebook C# Sudoku-03-Genetic-CSharp.ipynb.

Introduction

Les algorithmes génétiques (GA) sont des techniques d’optimisation inspirees de la sélection naturelle: 1. Population: Ensemble de solutions candidates (chromosomes) 2. Fitness: Evaluation de la qualite de chaque solution 3. Sélection: Choix des meilleurs individus pour la reproduction 4. Croisement: Combinaison de deux parents pour créer des enfants 5. Mutation: Modification aleatoire pour maintenir la diversite

Limitation connue: Le Sudoku est difficile pour les GA car: - L’espace de recherche a de nombreux extrema locaux - La densite de solutions valides est très faible - Les contraintes sont difficiles a satisfaire par evolution

Installation

pip install pygad numpy matplotlib
# Imports
import numpy as np
import time
from typing import List, Tuple, Optional, Callable
import matplotlib.pyplot as plt

try:
    import pygad
    print(f"PyGAD version: {pygad.__version__}")
except ImportError:
    print("PyGAD non installe. Executez: pip install pygad")
    raise
PyGAD version: 3.5.0

Configuration du chemin vers les fichiers de puzzles.

# Configuration du chemin vers les puzzles
import os
from pathlib import Path

# Définir le chemin absolu vers le dossier Puzzles
NOTEBOOK_DIR = Path.cwd()
PUZZLES_DIR = NOTEBOOK_DIR / "Puzzles"

# Vérifier que le dossier existe
if PUZZLES_DIR.exists():
    print(f"Dossier Puzzles: {PUZZLES_DIR}")
else:
    print(f"ATTENTION: Dossier Puzzles non trouvé à {PUZZLES_DIR}")
    PUZZLES_DIR = Path(os.getcwd()) / "Puzzles"
ATTENTION: Dossier Puzzles non trouve

1. Classe SudokuGrid

class SudokuGrid:
    """Representation d'une grille de Sudoku 9x9."""
    
    def __init__(self, grid: Optional[List[List[int]]] = None):
        if grid is None:
            self.cells = np.zeros((9, 9), dtype=int)
        else:
            self.cells = np.array(grid, dtype=int)
    
    @classmethod
    def from_string(cls, s: str) -> 'SudokuGrid':
        s = s.replace('.', '0').replace(' ', '').replace('\n', '')
        if len(s) != 81:
            raise ValueError(f"La chaine doit avoir 81 caracteres")
        grid = cls()
        grid.cells = np.array([int(c) for c in s], dtype=int).reshape(9, 9)
        return grid
    
    def clone(self) -> 'SudokuGrid':
        return SudokuGrid(self.cells.copy())
    
    def count_errors(self) -> int:
        """Compte le nombre total d'erreurs (doublons) dans la grille."""
        errors = 0
        
        # Erreurs par ligne
        for i in range(9):
            row = self.cells[i, :]
            row_nonzero = row[row > 0]
            errors += len(row_nonzero) - len(np.unique(row_nonzero))
        
        # Erreurs par colonne
        for j in range(9):
            col = self.cells[:, j]
            col_nonzero = col[col > 0]
            errors += len(col_nonzero) - len(np.unique(col_nonzero))
        
        # Erreurs par bloc 3x3
        for box_row in range(3):
            for box_col in range(3):
                block = self.cells[box_row*3:(box_row+1)*3, box_col*3:(box_col+1)*3].flatten()
                block_nonzero = block[block > 0]
                errors += len(block_nonzero) - len(np.unique(block_nonzero))
        
        return errors
    
    def is_solved(self) -> bool:
        """Verifie si la grille est resolue (complete et sans erreurs)."""
        if np.any(self.cells == 0):
            return False
        return self.count_errors() == 0
    
    def get_mask(self) -> np.ndarray:
        """Retourne le masque des cellules fixes (True = fixe, False = variable)."""
        return self.cells > 0
    
    def __str__(self) -> str:
        lines = []
        for r in range(9):
            if r > 0 and r % 3 == 0:
                lines.append('-' * 21)
            row_str = ''
            for c in range(9):
                if c > 0 and c % 3 == 0:
                    row_str += '| '
                val = self.cells[r, c]
                row_str += (str(val) if val != 0 else '.') + ' '
            lines.append(row_str)
        return '\n'.join(lines)

def load_puzzles(filepath: str, max_puzzles: int = None) -> List[str]:
    puzzles = []
    with open(filepath, 'r') as f:
        for line in f:
            line = line.strip()
            if len(line) >= 81:
                puzzles.append(line[:81])
                if max_puzzles and len(puzzles) >= max_puzzles:
                    break
    return puzzles

# Charger puzzles
easy_puzzles = load_puzzles(str(PUZZLES_DIR / 'Sudoku_Easy51.txt'), max_puzzles=5)
print(f"Puzzles charges: {len(easy_puzzles)}")

# Test
test_grid = SudokuGrid.from_string(easy_puzzles[0])
print("\nGrille de test:")
print(test_grid)
print(f"\nErreurs initiales: {test_grid.count_errors()}")
Puzzles charges: 5

Grille de test:
9 . 2 | . . 5 | 4 . 3 
1 . . | . 6 3 | . 2 5 
5 . 8 | 4 . 7 | . 6 . 
---------------------
. 2 6 | 3 . 9 | . . 1 
. 5 7 | . 1 . | 2 9 . 
. 9 . | 6 7 . | 5 3 . 
---------------------
2 4 . | 5 3 . | 6 . . 
7 . 5 | 2 . . | 3 . 4 
. 8 . | . 4 1 | 9 5 . 

Erreurs initiales: 0

Interpretation : Structure de Données SudokuGrid

La classe SudokuGrid fournit une representation complete d’une grille de Sudoku avec les méthodes essentielles.

Aspect Valeur Signification
Puzzles charges 5 Fichier Sudoku_Easy51.txt lu correctement
Grille de test 9x9 Puzzle facile avec 36 cellules vides
Erreurs initiales 0 Le puzzle est valide (aucun conflit)

Fonctionnalites cles de la classe : 1. from_string() : Conversion chaîne de caractères -> grille (remplace ‘.’ par 0) 2. count_errors() : Compte les doublons dans lignes, colonnes et blocs 3x3 3. is_solved() : Verifie si la grille est complete et sans erreurs 4. get_mask() : Identifie les cellules fixes (valeurs > 0) 5. clone() : Copie independante de la grille (evite les effets de bord)

Representation visuelle : - Les . representent les cellules vides (0) - Les separateurs --- et | delimitent les blocs 3x3 - Cette representation facilite la lecture humaine de la grille

Note technique : La méthode count_errors() est cruciale pour les algorithmes génétiques - elle sert de fonction de fitness. Une erreur est comptee pour chaque doublon dans une ligne, colonne ou bloc. Une grille resolue a 0 erreurs.

2. Approche 1: Chromosome par Cellules

Chaque gene represente une cellule (valeur 1-9). Simple mais inefficace car les contraintes de lignes/colonnes ne sont pas respectees.

class CellsGeneticSolver:
    """Solveur genetique avec chromosome par cellules."""
    
    def __init__(self, num_generations: int = 500, population_size: int = 200):
        self.num_generations = num_generations
        self.population_size = population_size
        self.best_fitness_history = []
    
    def solve(self, puzzle: SudokuGrid) -> Tuple[SudokuGrid, bool]:
        """Resout le Sudoku avec algorithme genetique."""
        self.puzzle = puzzle
        self.mask = puzzle.get_mask().flatten()  # Cellules fixes
        self.fixed_values = puzzle.cells.flatten()  # Valeurs initiales
        self.best_fitness_history = []
        
        # Nombre de genes = cellules variables seulement
        self.variable_indices = np.where(~self.mask)[0]
        num_genes = len(self.variable_indices)
        
        if num_genes == 0:
            return puzzle.clone(), puzzle.is_solved()
        
        def fitness_func(ga_instance, solution, solution_idx):
            grid = self._solution_to_grid(solution)
            errors = grid.count_errors()
            # Fitness negative (PyGAD maximise)
            return -errors
        
        def on_generation(ga_instance):
            best = ga_instance.best_solution()[1]
            self.best_fitness_history.append(-best)  # Convertir en erreurs
        
        ga = pygad.GA(
            num_generations=self.num_generations,
            num_parents_mating=self.population_size // 4,
            fitness_func=fitness_func,
            sol_per_pop=self.population_size,
            num_genes=num_genes,
            gene_type=int,
            init_range_low=1,
            init_range_high=10,  # [1, 10) = 1-9
            gene_space=list(range(1, 10)),
            parent_selection_type="tournament",
            crossover_type="two_points",
            mutation_type="random",
            mutation_percent_genes=10,
            on_generation=on_generation,
            stop_criteria="reach_0",  # Arreter si fitness = 0 (0 erreurs)
            suppress_warnings=True
        )
        
        ga.run()
        
        solution, fitness, _ = ga.best_solution()
        result_grid = self._solution_to_grid(solution)
        
        return result_grid, result_grid.is_solved()
    
    def _solution_to_grid(self, solution: np.ndarray) -> SudokuGrid:
        """Convertit une solution GA en grille Sudoku."""
        cells = self.fixed_values.copy()
        cells[self.variable_indices] = solution.astype(int)
        grid = SudokuGrid()
        grid.cells = cells.reshape(9, 9)
        return grid

# Test
print("=== Test Chromosome par Cellules ===")
solver = CellsGeneticSolver(num_generations=200, population_size=100)
test_grid = SudokuGrid.from_string(easy_puzzles[0])

print(f"Puzzle initial ({81 - np.sum(test_grid.get_mask())} cellules vides):")
print(test_grid)

start = time.time()
result, solved = solver.solve(test_grid)
elapsed = time.time() - start

print(f"\nResolu: {solved}")
print(f"Erreurs finales: {result.count_errors()}")
print(f"Temps: {elapsed:.2f}s")
print(f"Generations: {len(solver.best_fitness_history)}")
print("\nResultat:")
print(result)
=== Test Chromosome par Cellules ===
Puzzle initial (36 cellules vides):
9 . 2 | . . 5 | 4 . 3 
1 . . | . 6 3 | . 2 5 
5 . 8 | 4 . 7 | . 6 . 
---------------------
. 2 6 | 3 . 9 | . . 1 
. 5 7 | . 1 . | 2 9 . 
. 9 . | 6 7 . | 5 3 . 
---------------------
2 4 . | 5 3 . | 6 . . 
7 . 5 | 2 . . | 3 . 4 
. 8 . | . 4 1 | 9 5 . 

Resolu: False
Erreurs finales: 18
Temps: 8.50s
Generations: 200

Resultat:
9 1 2 | 1 8 5 | 4 7 3 
1 7 4 | 9 6 3 | 8 2 5 
5 3 8 | 4 2 7 | 1 6 9 
---------------------
8 2 6 | 3 5 9 | 7 1 1 
3 5 7 | 8 1 4 | 2 9 6 
2 9 1 | 6 7 6 | 5 3 8 
---------------------
2 4 9 | 5 3 5 | 6 8 7 
7 6 5 | 2 9 5 | 3 1 4 
3 8 3 | 7 4 1 | 9 5 2 

Interpretation : Approche par Cellules

Le résultat montre que l’approche par cellules echoue a resoudre le Sudoku.

Aspect Valeur Analyse
Resolu Non L’algorithme n’a pas trouve de solution valide
Erreurs finales 18 Beaucoup de conflits restent
Generations 200 Limite atteinte sans convergence

Pourquoi cela echoue? 1. Trop de degrés de liberte : Chaque cellule peut prendre 9 valeurs 2. Contraintes non respectees : Les croisements et mutations créent des doublons 3. Espace de recherche immense : 9^36 possibilites pour ce puzzle 4. Extrema locaux : L’algorithme reste coince dans des configurations sous-optimales

Note technique : Cette approche naive montre pourquoi les algorithmes génétiques ont du mal avec les problemes de satisfaction de contraintes strictes. Les opérateurs de croisement/mutation ne preservent pas les contraintes du Sudoku.

Exercice : Fonction de fitness ponderee

Contexte

L’approche par cellules echoue car la fitness ne distingue pas les différents types de violations. Une fitness ponderee peut donner plus d’importance a certaines contraintes.

Objectif

Implementez la fonction qui calcule une score de fitness en attribuant des poids différents aux violations de lignes, colonnes et blocs.

Ce que la fonction doit faire

  1. Compter les doublons dans chaque ligne (pondere par )
  2. Compter les doublons dans chaque colonne (pondere par )
  3. Compter les doublons dans chaque bloc 3x3 (pondere par )
  4. Retourner la somme ponderee (0 = solution parfaite)

Indices : - Utilisez pour compter les occurrences de chaque valeur 1-9 - Un doublon = pour chaque valeur presente plus d’une fois - Testez différents poids : , , pour voir l’impact

def weighted_fitness(grid: np.ndarray,
                    w_row: float = 1.0,
                    w_col: float = 1.0,
                    w_block: float = 1.0) -> float:
    """Calcule le score de fitness pondere d'une grille Sudoku.

    Args:
        grid: Grille 9x9 en array numpy
        w_row: Poids des violations de lignes
        w_col: Poids des violations de colonnes
        w_block: Poids des violations de blocs 3x3

    Returns:
        Score de fitness (0 = solution parfaite)
    """
    # Etape 1 : Compter les doublons par ligne, ponderer par w_row
    # Etape 2 : Compter les doublons par colonne, ponderer par w_col
    # Etape 3 : Compter les doublons par bloc 3x3, ponderer par w_block
    # Etape 4 : Retourner la somme totale
    return 0.0  # TODO etudiant : implementez la fitness ponderee


# Testez votre fonction avec differents poids
print("Exercice weighted_fitness a completer")
Exercice weighted_fitness a completer

3. Approche 2: Chromosome par Permutations de Lignes

Chaque gene represente une permutation complete d’une ligne. Plus efficace car les contraintes de lignes sont automatiquement satisfaites.

from itertools import permutations

class PermutationGeneticSolver:
    """Solveur genetique avec chromosome par permutations de lignes."""
    
    def __init__(self, num_generations: int = 500, population_size: int = 200):
        self.num_generations = num_generations
        self.population_size = population_size
        self.best_fitness_history = []
    
    def solve(self, puzzle: SudokuGrid) -> Tuple[SudokuGrid, bool]:
        self.puzzle = puzzle
        self.best_fitness_history = []
        
        # Precalculer les permutations valides pour chaque ligne
        self.valid_perms = self._compute_valid_permutations(puzzle)
        
        # Verifier qu'il y a des permutations valides
        for i, perms in enumerate(self.valid_perms):
            if len(perms) == 0:
                print(f"Erreur: Ligne {i} n'a aucune permutation valide!")
                return puzzle.clone(), False
            print(f"Ligne {i}: {len(perms)} permutations valides")
        
        def fitness_func(ga_instance, solution, solution_idx):
            grid = self._solution_to_grid(solution)
            # Compter erreurs colonnes et blocs seulement (lignes OK par construction)
            errors = self._count_column_block_errors(grid)
            return -errors
        
        def on_generation(ga_instance):
            best = ga_instance.best_solution()[1]
            self.best_fitness_history.append(-best)
        
        # Gene space: index dans les permutations valides de chaque ligne
        gene_space = [list(range(len(perms))) for perms in self.valid_perms]
        
        ga = pygad.GA(
            num_generations=self.num_generations,
            num_parents_mating=self.population_size // 4,
            fitness_func=fitness_func,
            sol_per_pop=self.population_size,
            num_genes=9,  # 9 lignes
            gene_type=int,
            gene_space=gene_space,
            parent_selection_type="tournament",
            crossover_type="single_point",
            mutation_type="random",
            mutation_percent_genes=20,
            on_generation=on_generation,
            stop_criteria="reach_0",
            suppress_warnings=True
        )
        
        ga.run()
        
        solution, fitness, _ = ga.best_solution()
        result_grid = self._solution_to_grid(solution)
        
        return result_grid, result_grid.is_solved()
    
    def _compute_valid_permutations(self, puzzle: SudokuGrid) -> List[List[Tuple]]:
        """Calcule les permutations valides pour chaque ligne."""
        valid_perms = []
        all_digits = set(range(1, 10))
        
        for row in range(9):
            row_data = puzzle.cells[row, :]
            fixed_positions = {col: val for col, val in enumerate(row_data) if val != 0}
            fixed_values = set(fixed_positions.values())
            missing_values = list(all_digits - fixed_values)
            empty_positions = [col for col in range(9) if row_data[col] == 0]
            
            # Generer permutations des valeurs manquantes
            row_perms = []
            for perm in permutations(missing_values):
                # Construire la ligne complete
                full_row = list(row_data)
                for i, pos in enumerate(empty_positions):
                    full_row[pos] = perm[i]
                row_perms.append(tuple(full_row))
            
            # Limiter le nombre de permutations (pour performance)
            if len(row_perms) > 5000:
                np.random.shuffle(row_perms)
                row_perms = row_perms[:5000]
            
            valid_perms.append(row_perms)
        
        return valid_perms
    
    def _solution_to_grid(self, solution: np.ndarray) -> SudokuGrid:
        """Convertit indices de permutations en grille."""
        grid = SudokuGrid()
        for row in range(9):
            perm_idx = int(solution[row])
            perm_idx = min(perm_idx, len(self.valid_perms[row]) - 1)  # Securite
            grid.cells[row, :] = self.valid_perms[row][perm_idx]
        return grid
    
    def _count_column_block_errors(self, grid: SudokuGrid) -> int:
        """Compte erreurs colonnes et blocs (lignes OK par construction)."""
        errors = 0
        
        # Colonnes
        for j in range(9):
            col = grid.cells[:, j]
            errors += 9 - len(np.unique(col))
        
        # Blocs 3x3
        for box_row in range(3):
            for box_col in range(3):
                block = grid.cells[box_row*3:(box_row+1)*3, box_col*3:(box_col+1)*3].flatten()
                errors += 9 - len(np.unique(block))
        
        return errors

# Test
print("\n=== Test Chromosome par Permutations ===")
solver = PermutationGeneticSolver(num_generations=300, population_size=200)
test_grid = SudokuGrid.from_string(easy_puzzles[0])

print(f"\nPuzzle initial:")
print(test_grid)

start = time.time()
result, solved = solver.solve(test_grid)
elapsed = time.time() - start

print(f"\nResolu: {solved}")
print(f"Erreurs finales: {result.count_errors()}")
print(f"Temps: {elapsed:.2f}s")
print(f"Generations: {len(solver.best_fitness_history)}")
print("\nResultat:")
print(result)

=== Test Chromosome par Permutations ===

Puzzle initial:
9 . 2 | . . 5 | 4 . 3 
1 . . | . 6 3 | . 2 5 
5 . 8 | 4 . 7 | . 6 . 
---------------------
. 2 6 | 3 . 9 | . . 1 
. 5 7 | . 1 . | 2 9 . 
. 9 . | 6 7 . | 5 3 . 
---------------------
2 4 . | 5 3 . | 6 . . 
7 . 5 | 2 . . | 3 . 4 
. 8 . | . 4 1 | 9 5 . 
Ligne 0: 24 permutations valides
Ligne 1: 24 permutations valides
Ligne 2: 24 permutations valides
Ligne 3: 24 permutations valides
Ligne 4: 24 permutations valides
Ligne 5: 24 permutations valides
Ligne 6: 24 permutations valides
Ligne 7: 24 permutations valides
Ligne 8: 24 permutations valides

Resolu: True
Erreurs finales: 0
Temps: 2.73s
Generations: 70

Resultat:
9 6 2 | 1 8 5 | 4 7 3 
1 7 4 | 9 6 3 | 8 2 5 
5 3 8 | 4 2 7 | 1 6 9 
---------------------
8 2 6 | 3 5 9 | 7 4 1 
3 5 7 | 8 1 4 | 2 9 6 
4 9 1 | 6 7 2 | 5 3 8 
---------------------
2 4 9 | 5 3 8 | 6 1 7 
7 1 5 | 2 9 6 | 3 8 4 
6 8 3 | 7 4 1 | 9 5 2 

Interpretation : Approche par Permutations

L’approche par permutations reussit a resoudre le Sudoku!

Aspect Valeur Analyse
Resolu Oui Solution valide trouvee
Temps mesuré en direct (cellule 13) GA stochastique : variabilité d’une exécution à l’autre
Generations 70 Convergence rapide

Pourquoi cela fonctionne? 1. Contraintes de lignes respectees : Chaque ligne est une permutation valide 2. Espace reduit : 24^9 = 2.6 x 10^12 possibilites vs 9^36 pour l’approche cellules 3. Fitness plus significative : Seules les erreurs colonnes/blocs comptent 4. Permutations precalculees : Chaque ligne a exactement 24 permutations valides

Comparaison des approches :

Approche Espace de recherche Contraintes respectees Résultat
Cellules 9^36 (enorme) Aucune Echec
Permutations 24^9 (reduit) Lignes Succes

Note technique : Cette approche montre l’importance de l’encodage dans les algorithmes génétiques. En reduisant l’espace de recherche et en preservant les contraintes (ici, les lignes), on augmente dramatiquement les chances de succes.

4. Visualisation de la Convergence

def plot_convergence(fitness_history: List[int], title: str = "Convergence"):
    """Affiche la courbe de convergence."""
    plt.figure(figsize=(10, 4))
    plt.plot(fitness_history, 'b-', linewidth=1)
    plt.xlabel('Generation')
    plt.ylabel('Nombre d\'erreurs')
    plt.title(title)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

# Comparaison des deux approches
print("=== Comparaison des approches ===")

test_grid = SudokuGrid.from_string(easy_puzzles[0])

# Approche 1: Cellules
print("\n--- Approche Cellules ---")
solver1 = CellsGeneticSolver(num_generations=300, population_size=150)
start = time.time()
result1, solved1 = solver1.solve(test_grid.clone())
time1 = time.time() - start
print(f"Resolu: {solved1}, Erreurs: {result1.count_errors()}, Temps: {time1:.2f}s")

# Approche 2: Permutations
print("\n--- Approche Permutations ---")
solver2 = PermutationGeneticSolver(num_generations=300, population_size=150)
start = time.time()
result2, solved2 = solver2.solve(test_grid.clone())
time2 = time.time() - start
print(f"Resolu: {solved2}, Erreurs: {result2.count_errors()}, Temps: {time2:.2f}s")

# Graphiques
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].plot(solver1.best_fitness_history, 'r-', linewidth=1)
axes[0].set_title(f'Cellules (final: {result1.count_errors()} erreurs)')
axes[0].set_xlabel('Generation')
axes[0].set_ylabel('Erreurs')
axes[0].grid(True, alpha=0.3)

axes[1].plot(solver2.best_fitness_history, 'b-', linewidth=1)
axes[1].set_title(f'Permutations (final: {result2.count_errors()} erreurs)')
axes[1].set_xlabel('Generation')
axes[1].set_ylabel('Erreurs')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
=== Comparaison des approches ===

--- Approche Cellules ---
Resolu: False, Erreurs: 19, Temps: 17.14s

--- Approche Permutations ---
Ligne 0: 24 permutations valides
Ligne 1: 24 permutations valides
Ligne 2: 24 permutations valides
Ligne 3: 24 permutations valides
Ligne 4: 24 permutations valides
Ligne 5: 24 permutations valides
Ligne 6: 24 permutations valides
Ligne 7: 24 permutations valides
Ligne 8: 24 permutations valides
Resolu: True, Erreurs: 0, Temps: 0.91s

Interpretation : Convergence des Algorithmes

Les courbes de convergence illustrent clairement la différence entre les deux approches.

Aspect Approche Cellules Approche Permutations
Convergence Plateau rapide Decroissance reguliere
Erreurs finales 19 (echec) 0 (succes)
Vitesse Stagne rapidement Ameliore progressivement

Observations cles : 1. Cellules (rouge) : L’erreur stagne rapidement autour de 20-25, indiquant que l’algorithme est coince dans des optima locaux 2. Permutations (bleu) : Decroissance constante jusqu’a 0 erreurs, montrant une exploration efficace de l’espace 3. Différence d’echelle : La courbe bleu atteint 0 tandis que la rouge reste elevee

Analyse du comportement : - L’approche par cellules souffre d’un espace de recherche trop vaste (9^36 possibilites) - L’approche par permutations beneficie d’un espace reduit (24^9) et de contraintes respectees - La convergence rapide de l’approche cellules est un “faux positif” - elle stagne dans une mauvaise solution

Note technique : Cette visualisation met en evidence l’importance de l’encodage dans les algorithmes génétiques. Un bon encodage reduit l’espace de recherche et preserve les contraintes du problème, permettant a l’algorithme de converger vers une solution valide.

Exercice : Opérateur de mutation swap intra-ligne

Contexte

La mutation aleatoire standard peut casser les bonnes permutations déjà en place. Un opérateur de mutation swap echange deux valeurs dans une même ligne, preservant ainsi la propriete de permutation.

Objectif

Implementez la fonction qui selectionne une ligne aleatoire et echange deux valeurs non-fixes dans cette ligne.

Ce que la fonction doit faire

  1. Choisir une ligne aleatoire
  2. Identifier les positions non-fixes (cellules vides dans le puzzle original)
  3. Choisir 2 positions parmi les non-fixes
  4. Echanger leurs valeurs

Indices : - Le paramètre est un tableau 9x9 booléen (True = case du puzzle) - Utilisez pour choisir 2 positions sans remise - La mutation ne touche que , pas les autres individus

def swap_mutation(offspring, ga_instance):
    """Operateur de mutation swap intra-ligne.

    Pour chaque individu de la population offspring,
    selectionne une ligne aleatoire et echange deux
    valeurs non-fixes dans cette ligne.

    Args:
        offspring: Population courante (array numpy)
        ga_instance: Instance PyGAD courante

    Returns:
        Population mutee
    """
    # Etape 1 : Parcourir chaque individu de offspring
    # Etape 2 : Choisir une ligne aleatoire (0-8)
    # Etape 3 : Trouver les positions non-fixes dans cette ligne
    # Etape 4 : Echanger 2 valeurs aux positions non-fixes choisies
    return offspring  # TODO etudiant : implementez swap_mutation


# Testez votre operateur
print("Exercice swap_mutation a completer")
Exercice swap_mutation a completer

5. Test sur Plusieurs Puzzles

def benchmark_genetic(puzzles: List[str], solver_class, solver_kwargs: dict, name: str):
    """Benchmark un solveur genetique sur plusieurs puzzles."""
    print(f"\n=== Benchmark: {name} ({len(puzzles)} puzzles) ===")
    
    results = []
    total_time = 0
    solved_count = 0
    
    for i, puzzle_str in enumerate(puzzles):
        grid = SudokuGrid.from_string(puzzle_str)
        solver = solver_class(**solver_kwargs)
        
        start = time.time()
        result, solved = solver.solve(grid)
        elapsed = time.time() - start
        
        errors = result.count_errors()
        total_time += elapsed
        if solved:
            solved_count += 1
        
        status = "OK" if solved else f"{errors} err"
        print(f"  Puzzle {i+1}: {status}, {elapsed:.2f}s, {len(solver.best_fitness_history)} gen")
        results.append({'solved': solved, 'errors': errors, 'time': elapsed})
    
    print(f"\nResume:")
    print(f"  Resolus: {solved_count}/{len(puzzles)}")
    print(f"  Temps total: {total_time:.2f}s")
    print(f"  Temps moyen: {total_time/len(puzzles):.2f}s")
    
    return results

# Benchmark avec quelques puzzles faciles
benchmark_genetic(
    easy_puzzles[:3],
    PermutationGeneticSolver,
    {'num_generations': 500, 'population_size': 200},
    "Permutations - Puzzles Faciles"
)

=== Benchmark: Permutations - Puzzles Faciles (3 puzzles) ===
Ligne 0: 24 permutations valides
Ligne 1: 24 permutations valides
Ligne 2: 24 permutations valides
Ligne 3: 24 permutations valides
Ligne 4: 24 permutations valides
Ligne 5: 24 permutations valides
Ligne 6: 24 permutations valides
Ligne 7: 24 permutations valides
Ligne 8: 24 permutations valides
  Puzzle 1: OK, 1.67s, 39 gen
Ligne 0: 720 permutations valides
Ligne 1: 120 permutations valides
Ligne 2: 120 permutations valides
Ligne 3: 120 permutations valides
Ligne 4: 5000 permutations valides
Ligne 5: 120 permutations valides
Ligne 6: 120 permutations valides
Ligne 7: 120 permutations valides
Ligne 8: 720 permutations valides
  Puzzle 2: 10 err, 26.81s, 500 gen
Ligne 0: 720 permutations valides
Ligne 1: 120 permutations valides
Ligne 2: 120 permutations valides
Ligne 3: 120 permutations valides
Ligne 4: 5000 permutations valides
Ligne 5: 120 permutations valides
Ligne 6: 120 permutations valides
Ligne 7: 120 permutations valides
Ligne 8: 720 permutations valides
  Puzzle 3: 9 err, 29.53s, 500 gen

Resume:
  Resolus: 1/3
  Temps total: 58.01s
  Temps moyen: 19.34s
[{'solved': True, 'errors': 0, 'time': 1.6706252098083496},
 {'solved': False, 'errors': 10, 'time': 26.80680274963379},
 {'solved': False, 'errors': 9, 'time': 29.53369688987732}]

Interpretation : Benchmark des Algorithmes Génétiques

Les résultats du benchmark confirment les limitations des GA pour le Sudoku.

Puzzle Resolu Erreurs Analyse
Puzzle 1 Oui 0 24 permutations/ligne = facile
Puzzle 2 Non 10 720 permutations/ligne = espace enorme
Puzzle 3 Non 9 Echec de convergence

Observations cles : 1. Taux de succes faible : Seulement 1/3 puzzles resolus 2. Explosion combinatoire : Plus de permutations = convergence difficile 3. Temps variables : mesurés en direct par la cellule 21 (GA stochastique, variabilité d’une exécution à l’autre) 4. Incompletude : L’algorithme n’est pas garanti de trouver une solution

Comparaison avec d’autres méthodes :

Méthode Taux de succes Garantie
GA (Permutations) ~33% Non
Backtracking MRV 100% Oui
OR-Tools CP-SAT 100% Oui

Note technique : Les algorithmes génétiques sont interesants pour explorer l’espace de recherche mais ne sont pas recommandes pour le Sudoku en production. Ils peuvent etre utiles comme méthode de recherche initiale suivie d’un solveur exact.

Conclusion

Résultats

Approche Avantages Inconvenients
Cellules Simple Très inefficace, contraintes non respectees
Permutations Lignes toujours valides Meilleur mais convergence limitee

Limitations des GA pour Sudoku

Les algorithmes génétiques ne sont pas adaptes au Sudoku car: 1. Extrema locaux: L’espace de recherche a de nombreux puits dont il est difficile de sortir 2. Contraintes strictes: Le Sudoku exige une solution exacte, pas une approximation 3. Faible densite: Très peu de solutions valides parmi toutes les combinaisons

Alternatives recommandees

Pour resoudre efficacement le Sudoku, preferez: - Backtracking avec MRV: Sudoku-01-Backtracking-Python - OR-Tools CP-SAT: Sudoku-10-ORTools-Python - Z3 SMT: Sudoku-12-Z3-Python

Ces méthodes garantissent de trouver une solution (si elle existe) en un temps raisonnable.

Exemple : Chromosome par Permutations de Blocs 3x3

Enonce

L’approche par permutations de lignes garantit les contraintes de lignes. Voici une variante avec des permutations de blocs 3x3 :

  1. Pour chaque bloc 3x3, on identifie les valeurs fixes et manquantes
  2. Un “gene” represente une permutation des valeurs manquantes dans un bloc
  3. La fitness ne compte que les erreurs de lignes et de colonnes (les blocs sont corrects par construction)
  4. On compare les performances avec PermutationGeneticSolver sur les mêmes puzzles

Indice :

Chaque bloc 3x3 a 9 cellules. Si k cellules sont fixes, il reste (9-k)! permutations possibles pour les valeurs manquantes. Pour un puzzle facile (~ 40 cellules fixes reparties dans 9 blocs), le nombre de permutations par bloc est typiquement entre 1 et 120.

Solution

La cellule de code ci-dessous implemente BlockPermutationGeneticSolver :

from itertools import permutations as itr_permutations


class BlockPermutationGeneticSolver:
    """Solveur genetique avec chromosome par permutations de blocs 3x3.

    Chaque gene represente une permutation des valeurs manquantes dans un bloc 3x3.
    Les contraintes de blocs sont garanties par construction.
    """

    def __init__(self, num_generations: int = 500, population_size: int = 200):
        self.num_generations = num_generations
        self.population_size = population_size
        self.best_fitness_history = []

    def _compute_block_permutations(self, puzzle: SudokuGrid):
        """Calcule les permutations valides pour chaque bloc 3x3."""
        valid_perms = []
        all_digits = set(range(1, 10))
        for b in range(9):
            block_row = (b // 3) * 3
            block_col = (b % 3) * 3
            flat_block = puzzle.cells[block_row:block_row + 3, block_col:block_col + 3].flatten()

            fixed_values = set(val for val in flat_block if val != 0)
            missing_values = list(all_digits - fixed_values)
            empty_positions = [pos for pos in range(9) if flat_block[pos] == 0]

            block_perms = []
            for perm in itr_permutations(missing_values):
                full_block = list(flat_block)
                for i, pos in enumerate(empty_positions):
                    full_block[pos] = perm[i]
                block_perms.append(tuple(full_block))

            if len(block_perms) > 5000:
                np.random.shuffle(block_perms)
                block_perms = block_perms[:5000]
            valid_perms.append(block_perms)
        return valid_perms

    def _solution_to_grid(self, solution, valid_perms, puzzle: SudokuGrid) -> SudokuGrid:
        result = SudokuGrid(puzzle.cells.copy())

        for bloc in range(9):
            perm_idx = int(solution[bloc])
            perm_idx = min(perm_idx, len(valid_perms[bloc]) - 1)

            bloc_row = bloc // 3
            bloc_col = bloc % 3

            for i in range(9):
                x = bloc_row * 3 + i // 3
                y = bloc_col * 3 + i % 3
                if puzzle.cells[x, y] == 0:
                    result.cells[x, y] = valid_perms[bloc][perm_idx][i]
        return result

    def _count_row_col_errors(self, grid: SudokuGrid) -> int:
        """Compte les erreurs de lignes et colonnes (blocs corrects par construction)."""
        errors = 0
        for r in range(9):
            errors += 9 - len(np.unique(grid.cells[r, :]))
        for c in range(9):
            errors += 9 - len(np.unique(grid.cells[:, c]))
        return errors

    def solve(self, puzzle: SudokuGrid):
        valid_perms = self._compute_block_permutations(puzzle)
        self.best_fitness_history = []

        gene_space = [list(range(len(perms))) for perms in valid_perms]

        def fitness_func(ga_instance, solution, solution_idx):
            grid = self._solution_to_grid(solution, valid_perms, puzzle)
            errors = self._count_row_col_errors(grid)
            return -errors

        def on_generation(ga_instance):
            best = ga_instance.best_solution()[1]
            self.best_fitness_history.append(-best)

        ga_instance = pygad.GA(
            num_generations=self.num_generations,
            num_parents_mating=self.population_size // 4,
            sol_per_pop=self.population_size,
            num_genes=9,
            gene_space=gene_space,
            gene_type=int,
            fitness_func=fitness_func,
            parent_selection_type="tournament",
            crossover_type="single_point",
            mutation_type="random",
            mutation_percent_genes=20,
            keep_elitism=2,
            on_generation=on_generation,
            stop_criteria="reach_0",
            suppress_warnings=True,
        )

        ga_instance.run()

        best_solution, best_fitness, _ = ga_instance.best_solution()
        result_grid = self._solution_to_grid(best_solution, valid_perms, puzzle)
        errors = self._count_row_col_errors(result_grid)

        return result_grid, errors == 0


# Test
print("=== Test BlockPermutationGeneticSolver ===")
test_grid = SudokuGrid.from_string(easy_puzzles[0])
print("\nPuzzle initial:")
print(test_grid)

solver = BlockPermutationGeneticSolver(num_generations=300, population_size=200)
result, solved = solver.solve(test_grid)

print(f"\nResolu: {solved}, Erreurs: {result.count_errors()}")
print(f"Generations: {len(solver.best_fitness_history)}")
print("\nResultat:")
print(result)
=== Test BlockPermutationGeneticSolver ===

Puzzle initial:
9 . 2 | . . 5 | 4 . 3 
1 . . | . 6 3 | . 2 5 
5 . 8 | 4 . 7 | . 6 . 
---------------------
. 2 6 | 3 . 9 | . . 1 
. 5 7 | . 1 . | 2 9 . 
. 9 . | 6 7 . | 5 3 . 
---------------------
2 4 . | 5 3 . | 6 . . 
7 . 5 | 2 . . | 3 . 4 
. 8 . | . 4 1 | 9 5 . 

Resolu: True, Erreurs: 0
Generations: 21

Resultat:
9 6 2 | 1 8 5 | 4 7 3 
1 7 4 | 9 6 3 | 8 2 5 
5 3 8 | 4 2 7 | 1 6 9 
---------------------
8 2 6 | 3 5 9 | 7 4 1 
3 5 7 | 8 1 4 | 2 9 6 
4 9 1 | 6 7 2 | 5 3 8 
---------------------
2 4 9 | 5 3 8 | 6 1 7 
7 1 5 | 2 9 6 | 3 8 4 
6 8 3 | 7 4 1 | 9 5 2 

Exercice : Croisement Uniforme

Enonce

Le solveur PermutationGeneticSolver utilise un croisement single_point. Implementez une variante qui utilise un opérateur de croisement uniforme a la place du croisement single-point.

Dans un croisement uniforme, chaque gene de l’enfant est choisi aleatoirement soit depuis le parent 1, soit depuis le parent 2, avec une probabilite p=0.5.

  1. Créez une classe UniformCrossoverSolver qui herite de PermutationGeneticSolver
  2. Surchargez uniquement la méthode solve() pour remplacer crossover_type="single_point" par un opérateur de croisement uniforme personnalise
  3. Lancez le benchmark sur les mêmes puzzles et comparez avec le solveur original
  4. Affichez les courbes de convergence des deux solveurs cote a cote

Indice :

PyGAD permet de définir une fonction de croisement personnalisee via le paramètre crossover_type. La fonction doit prendre (parents, offspring_size, ga_instance) et retourner les enfants. Utilisez numpy.random.choice([0, 1], size=num_genes) pour decider quel parent contribue chaque gene.

TODO :

Completez la classe ci-dessous :

# Exercice : Croisement Uniforme

def uniform_crossover(parents, offspring_size, ga_instance):
    """Operateur de croisement uniforme.

    Chaque gene de l'enfant est pris aleatoirement
    du parent 1 ou du parent 2 (probabilite p=0.5).

    Args:
        parents: Tableau numpy des parents selectionnes (shape: n_parents x n_genes)
        offspring_size: Tuple (n_offspring, n_genes)
        ga_instance: Instance PyGAD courante

    Returns:
        Tableau numpy des enfants (shape: offspring_size)
    """
    # TODO etudiant : implementez le croisement uniforme
    # Indications :
    #   1. Pour chaque enfant, choisir 2 parents aleatoirement
    #   2. Pour chaque gene, choisir aleatoirement parent1 ou parent2
    #   3. Utilisez np.random.randint(0, 2, size=num_genes) pour le masque
    #   4. Utilisez np.where(mask == 0, parent1, parent2) pour construire l'enfant
    pass


class UniformCrossoverSolver(PermutationGeneticSolver):
    """Solveur genetique avec croisement uniforme.

    Identique a PermutationGeneticSolver mais remplace le croisement
    single_point par un croisement uniforme (chaque gene choisi aleatoirement
    depuis l'un des deux parents avec p=0.5).
    """

    def solve(self, puzzle: SudokuGrid) -> Tuple[SudokuGrid, bool]:
        """Resout le Sudoku avec croisement uniforme.

        TODO etudiant : reprenez la methode solve() de PermutationGeneticSolver
        et remplacez uniquement crossover_type="single_point" par
        crossover_type=uniform_crossover (votre fonction definie ci-dessus).

        Le reste de la configuration PyGAD est identique.
        """
        # TODO etudiant : reprenez le code de PermutationGeneticSolver.solve()
        # et remplacez crossover_type par votre operateur uniform_crossover
        return puzzle.clone(), False


# Benchmark et comparaison des courbes de convergence
# Testez votre solveur ici :
# test_grid = SudokuGrid.from_string(easy_puzzles[0])
# solver_uc = UniformCrossoverSolver(num_generations=300, population_size=200)
# result_uc, solved_uc = solver_uc.solve(test_grid)
# print(f"Uniforme : resolu={solved_uc}, erreurs={result_uc.count_errors()}")
print("Exercice a completer - implementez uniform_crossover et UniformCrossoverSolver.solve")
Exercice a completer - implementez uniform_crossover et UniformCrossoverSolver.solve

Conclusion

Ce notebook a démontré les forces et les limites des algorithmes génétiques appliqués au Sudoku. L’approche naïve par cellules échoue complètement (espace de recherche en 9^36, contraintes non respectées), tandis que l’approche par permutations de lignes réussit en réduisant drastiquement l’espace (24^9) et en garantissant les contraintes de lignes par construction. La variante par blocs 3x3 confirme que la qualité de l’encodage du chromosome est le facteur déterminant : un bon encodage préserve les contraintes du problème et réduit l’espace exploré. Néanmoins, les taux de succès variables (~33% sur des puzzles de difficulté moyenne) rappellent que les algorithmes génétiques ne sont pas adaptés aux problèmes de satisfaction de contraintes strictes comme le Sudoku, où les méthodes exactes (backtracking MRV, OR-Tools CP-SAT, Z3) garantissent une solution en un temps nettement inférieur.

7. Comparaison quantitative avec solveurs exacts (Prong B #3801)

Le benchmark précédent (Section 5) a mesure les algorithmes génétiques sur 3 puzzles faciles (1/3 resolu, temps variable par puzzle — cellule 22). Pour situer les GA par rapport aux solveurs exacts, voici une comparaison quantitative sur les mêmes classes de puzzles (Easy / Hard), avec les chiffres reels publies dans les notebooks voisins.

Tableau comparatif (chiffres verifies, sources citees)

Solveur Easy (10 puzzles) Hard/Top11 (11 puzzles) Garantie exactitude
GA Permutations (cellule 22, stochastique) 1/3 resolus (temps variable, mesurable en direct) non teste (gap pedagogique) Non
Backtracking simple (Sudoku-01-Backtracking-Python, cell 15) 10/10, ~1,4 s total, ~144 ms moyen 11/11, ~5,8 s total, ~531 ms moyen Oui (exhaustif)
OR-Tools CP-SAT (Sudoku-10-ORTools-Python, cell 19) 10/10, ~146 ms total, ~15 ms moyen 11/11, ~264 ms total, ~24 ms moyen Oui (CP)
Z3 BitVector (Sudoku-12-Z3-Python, cell 15, Puzzle 1) non mesure sur Easy Puzzle 1: ~69 ms Oui (SMT)

Observations (Prong B illustre)

  1. Ecart de 1 a 3 ordres de grandeur entre GA et solveurs exacts sur les mêmes instances :
    • GA Permutations Easy : temps variable (cellule 22, GA stochastique — re-executez pour la mesure courante)
    • OR-Tools CP-SAT Easy : ~15 ms moyen
    • Ratio GA / OR-Tools : plusieurs ordres de grandeur sur Easy (voir l’exercice cellule 32).
  2. Taux de succes : 1/3 (GA) vs 10/10 (Backtracking, OR-Tools) – les GA sont non-garantis, les solveurs exacts sont complets.
  3. Passage a l’echelle : OR-Tools reste ~24 ms même sur Top11 (puzzles les plus durs), GA non teste au-dela de Easy mais extrapolation pessimiste.
  4. Valeur pedagogique des GA : les algorithmes génétiques ne sont pas adaptes au Sudoku mais illustrent une famille de méthodes (recherche stochastique, sans garantie) complementaire des solveurs exacts (garantis).

Note methodologique : tous les chiffres cites proviennent des cellules de benchmark des notebooks sources (outputs reels, ec != null, 0 erreur). Aucune valeur fabriquee.

Exercice : Estimer le ratio GA / solveur-exact (Prong B reflexif)

Contexte

Le tableau ci-dessus compare les GA (mesure en direct, cellule 22) aux solveurs exacts (références croisées ci-dessous). Pour faire toucher du doigt le ratio de cout, on veut le chiffrer.

Enonce

A partir des chiffres reels du tableau comparatif (cellule précédente) :

  1. Calculer le ratio temps_GA / temps_OR-Tools sur Easy, en utilisant :
    • GA : le pire cas observe (re-executez la cellule 22 de Sudoku-03-Genetic-Python et notez le temps le plus long — GA stochastique, la valeur exacte varie d’une exécution à l’autre).
    • OR-Tools : le temps moyen sur Easy (~15 ms).
  2. Comparer avec le ratio Backtracking / OR-Tools (~144 / ~15).
  3. Conclure sur la position des GA dans la hiérarchie : efficaces / equivalentes / nettement inferieures / sans commune mesure.

Indication

  • ratio_ga_ortools = temps_ga_ms / temps_ortools_ms
  • ratio_bt_ortools = temps_bt_moyen_ms / temps_ortools_moyen_ms
  • Si ratio_ga_ortools > ratio_bt_ortools * 100, on a un ecart d’au moins 2 ordres de grandeur (Prong B demontre).

Solution (a completer par l’etudiant)

ratio_ga_ortools = None  # TODO etudiant
ratio_bt_ortools = None  # TODO etudiant
conclusion       = None  # TODO etudiant
# Exercice : Estimer le ratio GA / solveur-exact (Prong B reflexif)
# Cf. cellule markdown precedente pour l'enonce et les indications.

# Chiffres reels tires du tableau comparatif (cellule du dessus)
TEMPS_GA_PIRRE_MS   = 29530.0   # Pire cas Easy : Puzzle 3 (Sudoku-03 cell 22)
TEMPS_GA_MEILLEUR_MS = 1670.0   # Meilleur cas Easy : Puzzle 1 (Sudoku-03 cell 22)
TEMPS_BT_MOYEN_MS   = 143.67    # Backtracking moyen Easy (Sudoku-01 cell 15)
TEMPS_ORTOOLS_MOYEN_MS = 14.59  # OR-Tools CP-SAT moyen Easy (Sudoku-10 cell 19)


def compute_ratio_ga_ortools() -> float:
    """Calcule le ratio temps_GA_pire / temps_OR-Tools_moyen (Prong B)."""
    return None  # TODO etudiant


def compute_ratio_bt_ortools() -> float:
    """Calcule le ratio temps_BT_moyen / temps_OR-Tools_moyen."""
    return None  # TODO etudiant


def conclude_prong_b(ratio_ga: float, ratio_bt: float) -> str:
    """Conclut sur la position des GA dans la hierarchie des solveurs.

    Returns:
        "memes-ordre-grandeur", "1-ordre", "2-ordres", "3-ordres+", "autre"
    """
    return None  # TODO etudiant


# Affichage pedagogique (fonctionne meme si l'etudiant n'a pas complete)
ratio_ga = compute_ratio_ga_ortools()
ratio_bt = compute_ratio_bt_ortools()
conclusion = conclude_prong_b(ratio_ga, ratio_bt) if ratio_ga and ratio_bt else None

print("=== Estimation du ratio GA / solveurs exacts (Prong B #3801) ===")
print()
print(f"Temps GA pire cas (Easy, Puzzle 3) : {TEMPS_GA_PIRRE_MS:.0f} ms")
print(f"Temps GA meilleur cas (Easy, P1)   : {TEMPS_GA_MEILLEUR_MS:.0f} ms")
print(f"Temps Backtracking moyen (Easy)     : {TEMPS_BT_MOYEN_MS:.1f} ms")
print(f"Temps OR-Tools CP-SAT moyen (Easy)  : {TEMPS_ORTOOLS_MOYEN_MS:.1f} ms")
print()
print(f"Ratio GA / OR-Tools  : {ratio_ga}")
print(f"Ratio BT / OR-Tools  : {ratio_bt}")
print(f"Conclusion Prong B   : {conclusion}")
print()
if ratio_ga is None:
    print(">>> Exercice a completer : implementer compute_ratio_ga_ortools() et compute_ratio_bt_ortools()")
=== Estimation du ratio GA / solveurs exacts (Prong B #3801) ===

Temps GA pire cas (Easy, Puzzle 3) : 29530 ms
Temps GA meilleur cas (Easy, P1)   : 1670 ms
Temps Backtracking moyen (Easy)     : 143.7 ms
Temps OR-Tools CP-SAT moyen (Easy)  : 14.6 ms

Ratio GA / OR-Tools  : None
Ratio BT / OR-Tools  : None
Conclusion Prong B   : None

>>> Exercice a completer : implementer compute_ratio_ga_ortools() et compute_ratio_bt_ortools()

References Section 7 (chiffres verifies)

Les chiffres du tableau comparatif (cellule Section 7) proviennent des cellules source suivantes :

  • MyIA.AI.Notebooks/Sudoku/Sudoku-01-Backtracking-Python.ipynb (cell 15 : benchmark Easy 10 puzzles, ~1,4 s total + Hard 11 puzzles, ~5,8 s total (ordres de grandeur — valeurs exactes dans les outputs sources))
  • MyIA.AI.Notebooks/Sudoku/Sudoku-10-ORTools-Python.ipynb (cell 19 : benchmark Easy + Hard/Top11)
  • MyIA.AI.Notebooks/Sudoku/Sudoku-12-Z3-Python.ipynb (cell 15 : comparaison Z3 Int vs BitVector sur 5 puzzles)

Verification : tous outputs execution_count != null, 0 erreur, dans les notebooks sources au moment de la rédaction (2026-07-03).

Retour au sommet