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
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
# Importsimport numpy as npimport timefrom typing import List, Tuple, Optional, Callableimport matplotlib.pyplot as plttry:import pygadprint(f"PyGAD version: {pygad.__version__}")exceptImportError: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 puzzlesimport osfrom pathlib import Path# Définir le chemin absolu vers le dossier PuzzlesNOTEBOOK_DIR = Path.cwd()PUZZLES_DIR = NOTEBOOK_DIR /"Puzzles"# Vérifier que le dossier existeif 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 isNone:self.cells = np.zeros((9, 9), dtype=int)else:self.cells = np.array(grid, dtype=int)@classmethoddef from_string(cls, s: str) ->'SudokuGrid': s = s.replace('.', '0').replace(' ', '').replace('\n', '')iflen(s) !=81:raiseValueError(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 griddef 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 lignefor i inrange(9): row =self.cells[i, :] row_nonzero = row[row >0] errors +=len(row_nonzero) -len(np.unique(row_nonzero))# Erreurs par colonnefor j inrange(9): col =self.cells[:, j] col_nonzero = col[col >0] errors +=len(col_nonzero) -len(np.unique(col_nonzero))# Erreurs par bloc 3x3for box_row inrange(3):for box_col inrange(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 errorsdef is_solved(self) ->bool:"""Verifie si la grille est resolue (complete et sans erreurs)."""if np.any(self.cells ==0):returnFalsereturnself.count_errors() ==0def get_mask(self) -> np.ndarray:"""Retourne le masque des cellules fixes (True = fixe, False = variable)."""returnself.cells >0def__str__(self) ->str: lines = []for r inrange(9):if r >0and r %3==0: lines.append('-'*21) row_str =''for c inrange(9):if c >0and c %3==0: row_str +='| ' val =self.cells[r, c] row_str += (str(val) if val !=0else'.') +' ' lines.append(row_str)return'\n'.join(lines)def load_puzzles(filepath: str, max_puzzles: int=None) -> List[str]: puzzles = []withopen(filepath, 'r') as f:for line in f: line = line.strip()iflen(line) >=81: puzzles.append(line[:81])if max_puzzles andlen(puzzles) >= max_puzzles:breakreturn puzzles# Charger puzzleseasy_puzzles = load_puzzles(str(PUZZLES_DIR /'Sudoku_Easy51.txt'), max_puzzles=5)print(f"Puzzles charges: {len(easy_puzzles)}")# Testtest_grid = SudokuGrid.from_string(easy_puzzles[0])print("\nGrille de test:")print(test_grid)print(f"\nErreurs initiales: {test_grid.count_errors()}")
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.
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
Compter les doublons dans chaque ligne (pondere par )
Compter les doublons dans chaque colonne (pondere par )
Compter les doublons dans chaque bloc 3x3 (pondere par )
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 totalereturn0.0# TODO etudiant : implementez la fitness ponderee# Testez votre fonction avec differents poidsprint("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 permutationsclass PermutationGeneticSolver:"""Solveur genetique avec chromosome par permutations de lignes."""def__init__(self, num_generations: int=500, population_size: int=200):self.num_generations = num_generationsself.population_size = population_sizeself.best_fitness_history = []def solve(self, puzzle: SudokuGrid) -> Tuple[SudokuGrid, bool]:self.puzzle = puzzleself.best_fitness_history = []# Precalculer les permutations valides pour chaque ligneself.valid_perms =self._compute_valid_permutations(puzzle)# Verifier qu'il y a des permutations validesfor i, perms inenumerate(self.valid_perms):iflen(perms) ==0:print(f"Erreur: Ligne {i} n'a aucune permutation valide!")return puzzle.clone(), Falseprint(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-errorsdef 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 inself.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 inrange(9): row_data = puzzle.cells[row, :] fixed_positions = {col: val for col, val inenumerate(row_data) if val !=0} fixed_values =set(fixed_positions.values()) missing_values =list(all_digits - fixed_values) empty_positions = [col for col inrange(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 inenumerate(empty_positions): full_row[pos] = perm[i] row_perms.append(tuple(full_row))# Limiter le nombre de permutations (pour performance)iflen(row_perms) >5000: np.random.shuffle(row_perms) row_perms = row_perms[:5000] valid_perms.append(row_perms)return valid_permsdef _solution_to_grid(self, solution: np.ndarray) -> SudokuGrid:"""Convertit indices de permutations en grille.""" grid = SudokuGrid()for row inrange(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 griddef _count_column_block_errors(self, grid: SudokuGrid) ->int:"""Compte erreurs colonnes et blocs (lignes OK par construction).""" errors =0# Colonnesfor j inrange(9): col = grid.cells[:, j] errors +=9-len(np.unique(col))# Blocs 3x3for box_row inrange(3):for box_col inrange(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# Testprint("\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() - startprint(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)
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.
=== 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
Choisir une ligne aleatoire
Identifier les positions non-fixes (cellules vides dans le puzzle original)
Choisir 2 positions parmi les non-fixes
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 choisiesreturn offspring # TODO etudiant : implementez swap_mutation# Testez votre operateurprint("Exercice swap_mutation a completer")
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
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 :
Pour chaque bloc 3x3, on identifie les valeurs fixes et manquantes
Un “gene” represente une permutation des valeurs manquantes dans un bloc
La fitness ne compte que les erreurs de lignes et de colonnes (les blocs sont corrects par construction)
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_permutationsclass 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_generationsself.population_size = population_sizeself.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 inrange(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 inrange(9) if flat_block[pos] ==0] block_perms = []for perm in itr_permutations(missing_values): full_block =list(flat_block)for i, pos inenumerate(empty_positions): full_block[pos] = perm[i] block_perms.append(tuple(full_block))iflen(block_perms) >5000: np.random.shuffle(block_perms) block_perms = block_perms[:5000] valid_perms.append(block_perms)return valid_permsdef _solution_to_grid(self, solution, valid_perms, puzzle: SudokuGrid) -> SudokuGrid: result = SudokuGrid(puzzle.cells.copy())for bloc inrange(9): perm_idx =int(solution[bloc]) perm_idx =min(perm_idx, len(valid_perms[bloc]) -1) bloc_row = bloc //3 bloc_col = bloc %3for i inrange(9): x = bloc_row *3+ i //3 y = bloc_col *3+ i %3if puzzle.cells[x, y] ==0: result.cells[x, y] = valid_perms[bloc][perm_idx][i]return resultdef _count_row_col_errors(self, grid: SudokuGrid) ->int:"""Compte les erreurs de lignes et colonnes (blocs corrects par construction).""" errors =0for r inrange(9): errors +=9-len(np.unique(grid.cells[r, :]))for c inrange(9): errors +=9-len(np.unique(grid.cells[:, c]))return errorsdef 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-errorsdef 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# Testprint("=== 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)
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.
Créez une classe UniformCrossoverSolver qui herite de PermutationGeneticSolver
Surchargez uniquement la méthode solve() pour remplacer crossover_type="single_point" par un opérateur de croisement uniforme personnalise
Lancez le benchmark sur les mêmes puzzles et comparez avec le solveur original
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 Uniformedef 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'enfantpassclass 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_crossoverreturn 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.
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).
Taux de succes : 1/3 (GA) vs 10/10 (Backtracking, OR-Tools) – les GA sont non-garantis, les solveurs exacts sont complets.
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.
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) :
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).
Comparer avec le ratio Backtracking / OR-Tools (~144 / ~15).
Conclure sur la position des GA dans la hiérarchie : efficaces / equivalentes / nettement inferieures / sans commune mesure.
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)."""returnNone# TODO etudiantdef compute_ratio_bt_ortools() ->float:"""Calcule le ratio temps_BT_moyen / temps_OR-Tools_moyen."""returnNone# TODO etudiantdef 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" """returnNone# 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 elseNoneprint("=== 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 isNone: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))