Sudoku-Python-DancingLinks : Dancing Links / Algorithm X (Python)

Navigation : << Sudoku-01 Backtracking Python | Index | Sudoku-03 Genetic Python >>

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Formuler le Sudoku comme un problème de couverture exacte 2. Implementer l’algorithme Dancing Links de Donald Knuth 3. Comprendre les structures de données de listes doublement chainees circulaires 4. Comparer les performances de DLX avec d’autres approches

Duree estimee : ~13 min | Prerequis : Sudoku-00 Environment | Lien : Voir aussi Sudoku-02 DancingLinks C#


Ce notebook implemente un solveur de Sudoku utilisant l’algorithme Dancing Links (DLX) de Donald Knuth. C’est l’equivalent Python du notebook C# Sudoku-02-DancingLinks-CSharp.ipynb.

Table des matieres

  1. Introduction théorique
  2. Le problème de couverture exacte
  3. Sudoku comme problème de couverture exacte
  4. L’algorithme X de Knuth
  5. Implementation Dancing Links
  6. Tests et benchmarks

References

1. Introduction théorique

Pourquoi DLX est-il efficace pour Sudoku?

Algorithme Complexite Avantage
Backtracking simple O(9^81) pire cas Simple a implementer
Backtracking + MRV O(9^m) m=cases vides Bonne heuristique
Dancing Links O(n) opérations par noeud Optimal pour couverture exacte

DLX est particulierement adapte car: 1. Suppression/Restauration O(1) : Grace aux listes doublement chainees 2. Pas de copie de données : Les noeuds sont simplement “deconnectes” puis “reconnectes” 3. Heuristique MRV integree : Choix de la colonne avec le moins de 1s

2. Le problème de couverture exacte

Definition

Etant donne: - Un ensemble U = {1, 2, 3, …, n} d’éléments - Une collection S = {S1, S2, …, Sm} de sous-ensembles de U

Trouver une couverture exacte: une sous-collection S* de S telle que chaque élément de U appartient a exactement un sous-ensemble de S*.

Exemple simple

U = {1, 2, 3, 4, 5, 6, 7}

S = {
  A = {1, 4, 7}
  B = {1, 4}
  C = {4, 5, 7}
  D = {3, 5, 6}
  E = {2, 3, 6, 7}
  F = {2, 7}
}

Solution: S* = {B, D, F} car: - B couvre {1, 4} - D couvre {3, 5, 6} - F couvre {2, 7} - Union = {1, 2, 3, 4, 5, 6, 7} = U (chaque élément exactement une fois)

Representation matricielle

On represente le problème par une matrice binaire: - Chaque ligne = un sous-ensemble - Chaque colonne = un élément de U - M[i,j] = 1 si l’élément j est dans le sous-ensemble i

     1  2  3  4  5  6  7
A = [1, 0, 0, 1, 0, 0, 1]
B = [1, 0, 0, 1, 0, 0, 0]
C = [0, 0, 0, 1, 1, 0, 1]
D = [0, 0, 1, 0, 1, 1, 0]
E = [0, 1, 1, 0, 0, 1, 1]
F = [0, 1, 0, 0, 0, 0, 1]

Une couverture exacte = sélection de lignes ou chaque colonne a exactement un 1.

3. Sudoku comme problème de couverture exacte

Les 4 types de contraintes du Sudoku

Un Sudoku 9x9 standard a 324 contraintes (colonnes) reparties en 4 catégories:

Type Description Nombre Colonnes
Cell Chaque cellule contient exactement un chiffre 81 0-80
Row Chaque ligne contient chaque chiffre 1-9 81 81-161
Column Chaque colonne contient chaque chiffre 1-9 81 162-242
Box Chaque bloc 3x3 contient chaque chiffre 1-9 81 243-323

Les 729 possibilites (lignes)

Chaque ligne de la matrice represente le placement d’un chiffre v (1-9) dans une cellule (r, c):

  • 729 lignes = 9 lignes x 9 colonnes x 9 valeurs
  • Chaque ligne a exactement 4 bits a 1 (une contrainte de chaque type)

Calcul des indices de colonnes

Pour un placement (row=r, col=c, value=v):

# Contrainte Cell: cellule (r,c) est remplie
cell_col = r * 9 + c                           # 0-80

# Contrainte Row: ligne r contient valeur v
row_col = 81 + r * 9 + (v - 1)                 # 81-161

# Contrainte Column: colonne c contient valeur v  
col_col = 162 + c * 9 + (v - 1)                # 162-242

# Contrainte Box: bloc b contient valeur v
box = (r // 3) * 3 + (c // 3)
box_col = 243 + box * 9 + (v - 1)              # 243-323

Exemple visuel

Placer le chiffre 5 en position (2, 3) (ligne 2, colonne 3):

Contrainte Cell:   colonne 2*9+3 = 21
Contrainte Row:    colonne 81 + 2*9 + 4 = 103  
Contrainte Column: colonne 162 + 3*9 + 4 = 193
Contrainte Box:    colonne 243 + 0*9 + 4 = 247  (bloc 0)

Ligne de la matrice: [0...1...0] avec des 1 aux positions 21, 103, 193, 247

4. L’algorithme X de Knuth

Pseudo-code

function solve(matrix):
    if matrix is empty:
        return SUCCESS  # Solution trouvee!
    
    # Choisir la colonne c avec le moins de 1s (heuristique MRV)
    c = column_with_minimum_ones(matrix)
    
    if c has no 1s:
        return FAILURE  # Impasse
    
    # Pour chaque ligne r ayant un 1 dans la colonne c
    for each row r where matrix[r][c] == 1:
        # Ajouter r a la solution partielle
        solution.add(r)
        
        # Couvrir: supprimer c et toutes les lignes en conflit
        cover(c)
        for each column j where matrix[r][j] == 1:
            cover(j)
        
        # Recursion
        result = solve(reduced_matrix)
        if result == SUCCESS:
            return SUCCESS
        
        # Backtrack: restaurer les colonnes
        for each column j where matrix[r][j] == 1 (reverse order):
            uncover(j)
        uncover(c)
        solution.remove(r)
    
    return FAILURE

Exercice : Construire la matrice de couverture exacte pour un mini-Sudoku

Objectif Construisez manuellement la matrice de couverture exacte pour un Sudoku 4x4 (et non 9x9), ou chaque ligne/colonne/bloc 2x2 doit contenir les chiffres 1 a 4.

Indice Identifiez les 4 types de contraintes (ligne, colonne, bloc, cellule) et comptez le nombre de colonnes de la matrice. Pour chaque assignation possible (cellule, valeur), créez une ligne dans la matrice avec des 1 aux colonnes des contraintes satisfaites.

# EXERCICE : Construire la matrice de couverture exacte pour un mini-Sudoku 4x4
def build_mini_sudoku_matrix() -> list:
    # TODO: Construisez la matrice binaire de couverture exacte
    # pour un Sudoku 4x4 (chiffres 1-4, blocs 2x2)
    # Retournez une liste de listes (lignes = assignations, colonnes = contraintes)
    result = None  # TODO etudiant
    return result
print("Exercice a completer")
Exercice a completer
# Imports
import time
from typing import List, Optional, Set, Tuple, Generator
from dataclasses import dataclass, field

print("Imports OK")
Imports OK

6. Tests et benchmarks

6.1 Test basique

# Puzzle de test
test_puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079"

puzzle = SudokuGrid.from_string(test_puzzle)
print("Puzzle initial:")
print(puzzle)
print()

# Resoudre
solver = DLXSudokuSolver()
start = time.time()
solution = solver.solve(puzzle)
elapsed = (time.time() - start) * 1000

if solution:
    print(f"Solution trouvee en {elapsed:.2f} ms:")
    print(solution)
    print(f"\nSolution valide: {solution.is_valid()}")
else:
    print("Pas de solution!")
Puzzle initial:
5 3 . | . 7 . | . . . 
6 . . | 1 9 5 | . . . 
. 9 8 | . . . | . 6 . 
---------------------
8 . . | . 6 . | . . 3 
4 . . | 8 . 3 | . . 1 
7 . . | . 2 . | . . 6 
---------------------
. 6 . | . . . | 2 8 . 
. . . | 4 1 9 | . . 5 
. . . | . 8 . | . 7 9 

Solution trouvee en 4.74 ms:
5 3 4 | 6 7 8 | 9 1 2 
6 7 2 | 1 9 5 | 3 4 8 
1 9 8 | 3 4 2 | 5 6 7 
---------------------
8 5 9 | 7 6 1 | 4 2 3 
4 2 6 | 8 5 3 | 7 9 1 
7 1 3 | 9 2 4 | 8 5 6 
---------------------
9 6 1 | 5 3 7 | 2 8 4 
2 8 7 | 4 1 9 | 6 3 5 
3 4 5 | 2 8 6 | 1 7 9 

Solution valide: True

Interpretation : Resolution du puzzle de test

Le solveur DLX a resolu le puzzle en (ms live – regle #9434), ce qui est excellent.

Aspect Valeur Signification
Temps de resolution (ms live – regle #9434) Très rapide, même pour un puzzle de difficulte moyenne
Solution valide True L’algorithme a trouve une solution correcte
Cases vides initiales 51 Puzzle relativement difficile (presque la moitie de la grille)

Points cles : 1. Performance immediatement optimale : DLX n’a pas besoin d’heuristiques additionnelles 2. Resolution correcte : La solution respecte toutes les contraintes Sudoku 3. Comparaison favorable : (ms live – regle #9434) est largement plus rapide que le backtracking simple sur des puzzles similaires

Note technique : Le temps de resolution inclut la construction de la matrice de couverture exacte (324 colonnes x 729 lignes maximum) et l’exécution de l’algorithme X. Malgre cette surcharge initiale, DLX reste extremement rapide car la structure de données est optimisee pour ce type de problème.

Exercice : Verifier une solution DLX

Contexte

Le solveur DLX trouve une solution au Sudoku en construisant une matrice de couverture exacte. Mais comment etre sur que la solution est correcte ? Il faut verifier que toutes les contraintes sont couvertes exactement une fois.

Objectif

Implementez la fonction qui verifie qu’une solution Dancing Links est une couverture exacte valide pour un Sudoku 9x9.

Ce que la fonction doit verifier

  1. Le nombre de lignes selectionnees est 81 (une par case du Sudoku)
  2. Chacune des 324 contraintes (colonnes) est couverte exactement une fois
  3. Aucune contrainte n’est couverte zero fois ni plusieurs fois

Indices : - Pour chaque ligne selectionnee, utilisez pour obtenir les 4 colonnes couvertes - Utilisez un tableau de taille 324 pour compter les occurrences - La couverture exacte signifie : chaque colonne a un compte de exactement 1

def verify_dlx_solution(solution_rows: list, row_info: dict,
                         solver: DLXSudokuSolver) -> bool:
    """Verifie qu'une solution DLX est une couverture exacte valide.

    Args:
        solution_rows: Liste des row_ids selectionnes par DLX
        row_info: Dictionnaire row_id -> (r, c, v)
        solver: Instance de DLXSudokuSolver pour acceder a _get_columns

    Returns:
        True si la solution est une couverture exacte valide, False sinon
    """
    # Etape 1 : Verifier qu'on a exactement 81 lignes (une par case)
    # Etape 2 : Compter combien de fois chaque contrainte (colonne 0-323) est couverte
    # Etape 3 : Verifier que les 324 contraintes sont couvertes exactement 1 fois
    return False  # TODO etudiant : implementez la verification


# Test de votre implementation
print("Exercice verify_dlx_solution a completer")
Exercice verify_dlx_solution a completer

6.2 Chargement des puzzles depuis fichiers

def load_puzzles(filepath: str, max_puzzles: int = None) -> List[str]:
    """Charge les puzzles depuis un fichier.
    
    Chaque ligne du fichier doit contenir au moins 81 caracteres
    representant un puzzle (0 ou . pour les cases vides).
    
    Args:
        filepath: Chemin vers le fichier
        max_puzzles: Nombre maximum de puzzles a charger
    
    Returns:
        Liste de chaines de 81 caracteres
    """
    puzzles = []
    try:
        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
    except FileNotFoundError:
        print(f"Fichier non trouve: {filepath}")
    return puzzles

# Charger les puzzles
from pathlib import Path
NOTEBOOK_DIR = Path.cwd()
PUZZLES_DIR = NOTEBOOK_DIR / "Puzzles"

easy_puzzles = load_puzzles(str(PUZZLES_DIR / 'Sudoku_Easy51.txt'), max_puzzles=10)
hard_puzzles = load_puzzles(str(PUZZLES_DIR / 'Sudoku_hardest.txt'))

print(f"Puzzles faciles charges: {len(easy_puzzles)}")
print(f"Puzzles difficiles charges: {len(hard_puzzles)}")
Puzzles faciles charges: 10
Puzzles difficiles charges: 11

Interpretation : Chargement des puzzles

Les fichiers de puzzles ont ete charges avec succes depuis le dossier Puzzles/.

Fichier Statut Contenu
Sudoku_Easy51.txt Charge 10 puzzles faciles
Sudoku_hardest.txt Charge 11 puzzles difficiles

Utilisation : 1. Les puzzles faciles alimentent le benchmark de la section suivante 2. Les puzzles difficiles (Top 11) testent DLX sur des grilles plus contraintes 3. Un puzzle hardcode (section 6.1) reste disponible comme exemple autonome

Note technique : Le chargement lit les fichiers .txt du dossier Puzzles/ ; un puzzle hardcode (section 6.1) reste disponible comme exemple autonome.

Conclusion et comparaison

Performances attendues

Algorithme Puzzle facile Puzzle difficile
Backtracking simple 1-10 ms 100-1000 ms
Backtracking + MRV 0.5-5 ms 10-100 ms
Dancing Links (mesure) ~3.6 ms ~9.6 ms
OR-Tools CP-SAT 1-5 ms 5-20 ms
Z3 SMT 5-20 ms 20-100 ms

Seule la ligne Dancing Links est mesuree dans ce notebook (benchmark section 6) ; les autres lignes sont des ordres de grandeur indicatifs issus de leurs notebooks respectifs.

Inconvenients

  1. Complexite d’implementation plus elevee
  2. Spécifique aux problemes de couverture exacte
  3. Memoire utilisee pour les pointeurs

Navigation : << Sudoku-01 Backtracking Python | Index | Sudoku-03 Genetic Python >>

Retour au sommet