Créer un solveur de sudoku en Java

1. Vue d’ensemble

Dans cet article, nous allons examiner le puzzle de Sudoku et les algorithmes utilisés pour le résoudre.

Nous allons ensuite implémenter des solutions en Java. La première solution sera une simple attaque par force brute. La seconde utilisera la technique Dancing Links .

Gardons à l’esprit que nous allons nous concentrer sur les algorithmes et non sur la conception POO

2. Le Sudoku

En termes simples, Sudoku est un casse-tête combinatoire de placement de nombres avec une grille de 9 x 9 cellules partiellement remplie avec des nombres de 1 à 9. Le but est de remplir les champs vides restants avec les nombres restants de manière à ce que chaque ligne et colonne ne comporte qu’un seul. nombre de chaque sorte.

De plus, chaque sous-section 3 x 3 de la grille ne peut pas non plus dupliquer un nombre. Le niveau de difficulté augmente naturellement avec le nombre de champs vides dans chaque tableau.

2.1. Test Board

Pour rendre notre solution plus intéressante et valider l’algorithme, nous allons utiliser le http://www.telegraph.co.uk/news/science/science-news/9359579/Worlds-hardest-sudoku-can-you -crack-it.html[“Le sudoku le plus dur au monde”], qui est:

8 . . . . . . . .
. . 3 6 . . . . .
. 7 . . 9 . 2 . .
. 5 . . . 7 . . .
. . . . 4 5 7 . .
. . . 1 . . . 3 .
. . 1 . . . . 6 8
. . 8 5 . . . 1 .
. 9 . . . . 4 . .

2.2. Conseil résolu

Et, pour gâcher rapidement la solution, le puzzle correctement résolu donnera le résultat suivant:

8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2

3. Algorithme de retour en arrière

3.1. Introduction

  • L’algorithme de retour en arrière tente de résoudre le problème en testant chaque cellule pour trouver une solution valable. **

S’il n’y a pas violation des contraintes, l’algorithme passe à la cellule suivante, remplit toutes les solutions potentielles et répète toutes les vérifications.

En cas de violation, la valeur de la cellule est incrémentée. Une fois que la valeur de la cellule atteint 9 et qu’il y a toujours violation, l’algorithme revient à la cellule précédente et augmente la valeur de cette cellule.

Il essaie toutes les solutions possibles.

3.2. Solution

Tout d’abord, définissons notre tableau comme un tableau d’entiers à deux dimensions. Nous utiliserons 0 comme notre cellule vide.

int[][]board = {
  { 8, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 3, 6, 0, 0, 0, 0, 0 },
  { 0, 7, 0, 0, 9, 0, 2, 0, 0 },
  { 0, 5, 0, 0, 0, 7, 0, 0, 0 },
  { 0, 0, 0, 0, 4, 5, 7, 0, 0 },
  { 0, 0, 0, 1, 0, 0, 0, 3, 0 },
  { 0, 0, 1, 0, 0, 0, 0, 6, 8 },
  { 0, 0, 8, 5, 0, 0, 0, 1, 0 },
  { 0, 9, 0, 0, 0, 0, 4, 0, 0 }
};

Créons une méthode solve () qui prend board en tant que paramètre d’entrée et effectue une itération sur des lignes, des colonnes et des valeurs en testant chaque cellule pour rechercher une solution valide:

private boolean solve(int[][]board) {
    for (int row = BOARD__START__INDEX; row < BOARD__SIZE; row++) {
        for (int column = BOARD__START__INDEX; column < BOARD__SIZE; column++) {
            if (board[row][column]== NO__VALUE) {
                for (int k = MIN__VALUE; k <= MAX__VALUE; k++) {
                    board[row][column]= k;
                    if (isValid(board, row, column) && solve(board)) {
                        return true;
                    }
                    board[row][column]= NO__VALUE;
                }
                return false;
            }
        }
    }
    return true;
}

Une autre méthode dont nous avions besoin est la méthode isValid () , qui vérifie les contraintes Sudoku, c’est-à-dire vérifie si la ligne, la colonne et la grille 3 x 3 sont valides:

private boolean isValid(int[][]board, int row, int column) {
    return (rowConstraint(board, row)
      && columnConstraint(board, column)
      && subsectionConstraint(board, row, column));
}

Ces trois contrôles sont relativement similaires. Commençons d’abord par les vérifications de rangées:

private boolean rowConstraint(int[][]board, int row) {
    boolean[]constraint = new boolean[BOARD__SIZE];
    return IntStream.range(BOARD__START__INDEX, BOARD__SIZE)
      .allMatch(column -> checkConstraint(board, row, constraint, column));
}

Ensuite, nous utilisons un code presque identique pour valider la colonne:

private boolean columnConstraint(int[][]board, int column) {
    boolean[]constraint = new boolean[BOARD__SIZE];
    return IntStream.range(BOARD__START__INDEX, BOARD__SIZE)
      .allMatch(row -> checkConstraint(board, row, constraint, column));
}

De plus, nous devons valider la sous-section 3 x 3:

private boolean subsectionConstraint(int[][]board, int row, int column) {
    boolean[]constraint = new boolean[BOARD__SIZE];
    int subsectionRowStart = (row/SUBSECTION__SIZE) **  SUBSECTION__SIZE;
    int subsectionRowEnd = subsectionRowStart + SUBSECTION__SIZE;

    int subsectionColumnStart = (column/SUBSECTION__SIZE) **  SUBSECTION__SIZE;
    int subsectionColumnEnd = subsectionColumnStart + SUBSECTION__SIZE;

    for (int r = subsectionRowStart; r < subsectionRowEnd; r++) {
        for (int c = subsectionColumnStart; c < subsectionColumnEnd; c++) {
            if (!checkConstraint(board, r, constraint, c)) return false;
        }
    }
    return true;
}

Enfin, nous avons besoin d’une méthode checkConstraint () :

boolean checkConstraint(
  int[][]board,
  int row,
  boolean[]constraint,
  int column) {
    if (board[row][column]!= NO__VALUE) {
        if (!constraint[board[row][column]- 1]) {
            constraint[board[row][column]- 1]= true;
        } else {
            return false;
        }
    }
    return true;
}

Une fois que tout est fait, la méthode isValid () peut simplement renvoyer true .

Nous sommes presque prêts à tester la solution maintenant. L’algorithme est terminé.

Cependant, il retourne true ou false uniquement.

Par conséquent, pour vérifier visuellement le tableau, nous devons simplement imprimer le résultat. Apparemment, cela ne fait pas partie de l’algorithme.

private void printBoard() {
    for (int row = BOARD__START__INDEX; row < BOARD__SIZE; row++) {
        for (int column = BOARD__START__INDEX; column < BOARD__SIZE; column++) {
            System.out.print(board[row][column]+ " ");
        }
        System.out.println();
    }
}

Nous avons implémenté avec succès l’algorithme de retour en arrière qui résout le puzzle du Sudoku!

De toute évidence, il y a place à des améliorations car l’algorithme vérifie naïvement chaque combinaison possible (même si nous savons que la solution particulière est invalide).

4. Liens de danse

4.1. Couverture exacte

Regardons une autre solution. Sudoku peut être décrit comme un problème Exact Cover , qui peut être représenté par une matrice d’incidence montrant la relation entre deux objets.

Par exemple, si nous prenons les nombres de 1 à 7 et la collection d’ensembles S = \ {A, B, C, D, E, F} , où:

  • A = \ {1, 4, 7}

  • B = \ {1, 4}

  • C = \ {4, 5, 7}

  • D = \ {3, 5, 6}

  • E = \ {2, 3, 6, 7}

  • F = \ {2, 7}

Notre objectif est de sélectionner des sous-ensembles tels que chaque numéro n’y figure qu’une fois et qu’aucun ne se répète, d’où le nom.

Nous pouvons représenter le problème en utilisant une matrice, où les colonnes sont des nombres et les lignes sont des ensembles:

  | 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 |

La sous-collection S ** = \ {B, D, F} est une couverture exacte:

  | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |

Chaque colonne a exactement un 1 dans toutes les lignes sélectionnées.

4.2. Algorithme X

L’algorithme X est une «approche empirique» pour trouver toutes les solutions au problème de couverture exact, c’est-à-dire à partir de notre collection d’exemples S = \ {A, B, C, D, E, F} , trouver une sous-collection S ** = \ {B, D, F} .

L’algorithme X fonctionne comme suit:

  1. Si la matrice A n’a pas de colonne, la solution partielle actuelle est une

solution valable; terminer avec succès, sinon, choisissez une colonne c (déterministe) . Choisissez une ligne r telle que _A ~ r , c_ ~ = 1 (non déterministe,

c’est-à-dire essayer toutes les possibilités) . Inclure la ligne r dans la solution partielle

  1. Pour chaque colonne j telle que _A ~ r , j ~ = 1, pour chaque ligne i_

tel que _A ~ i , j ~ = 1, supprimer la ligne i de la matrice A et supprimer la colonne j de la matrice A . Répétez cet algorithme de manière récursive sur la matrice réduite A_

Une implémentation efficace de l’algorithme X est l’algorithme Dancing Links (DLX en abrégé) suggéré par le Dr Donald Knuth.

La solution suivante a été largement inspirée par la mise en œuvre de cette Java.

4.3. Problème de couverture exacte

Premièrement, nous devons créer une matrice qui représentera le puzzle de Sudoku en tant que problème de la couverture exacte. La matrice comportera 9 ^ 3 lignes, c’est-à-dire une ligne pour chaque position possible (9 lignes x 9 colonnes) de chaque nombre possible (9 chiffres).

Les colonnes représenteront le tableau (encore 9 x 9) multiplié par le nombre de contraintes.

Nous avons déjà défini trois contraintes:

  • chaque ligne aura un seul numéro de chaque type

  • chaque colonne aura un seul numéro de chaque type

  • chaque sous-section aura un seul numéro de chaque type

De plus, il existe une quatrième contrainte implicite:

  • un seul numéro peut être dans une cellule

Cela donne quatre contraintes au total et donc 9 x 9 x 4 colonnes dans la matrice Couverture exacte:

private static int BOARD__SIZE = 9;
private static int SUBSECTION__SIZE = 3;
private static int NO__VALUE = 0;
private static int CONSTRAINTS = 4;
private static int MIN__VALUE = 1;
private static int MAX__VALUE = 9;
private static int COVER__START__INDEX = 1;
private int getIndex(int row, int column, int num) {
    return (row - 1) **  BOARD__SIZE **  BOARD__SIZE
      + (column - 1) **  BOARD__SIZE + (num - 1);
}
private boolean[][]createExactCoverBoard() {
    boolean[][]coverBoard = new boolean
     [BOARD__SIZE **  BOARD__SIZE **  MAX__VALUE]     [BOARD__SIZE **  BOARD__SIZE **  CONSTRAINTS];

    int hBase = 0;
    hBase = checkCellConstraint(coverBoard, hBase);
    hBase = checkRowConstraint(coverBoard, hBase);
    hBase = checkColumnConstraint(coverBoard, hBase);
    checkSubsectionConstraint(coverBoard, hBase);

    return coverBoard;
}

private int checkSubsectionConstraint(boolean[][]coverBoard, int hBase) {
    for (int row = COVER__START__INDEX; row <= BOARD__SIZE; row += SUBSECTION__SIZE) {
        for (int column = COVER__START__INDEX; column <= BOARD__SIZE; column += SUBSECTION__SIZE) {
            for (int n = COVER__START__INDEX; n <= BOARD__SIZE; n++, hBase++) {
                for (int rowDelta = 0; rowDelta < SUBSECTION__SIZE; rowDelta++) {
                    for (int columnDelta = 0; columnDelta < SUBSECTION__SIZE; columnDelta++) {
                        int index = getIndex(row + rowDelta, column + columnDelta, n);
                        coverBoard[index][hBase]= true;
                    }
                }
            }
        }
    }
    return hBase;
}

private int checkColumnConstraint(boolean[][]coverBoard, int hBase) {
    for (int column = COVER__START__INDEX; column <= BOARD__SIZE; c++) {
        for (int n = COVER__START__INDEX; n <= BOARD__SIZE; n++, hBase++) {
            for (int row = COVER__START__INDEX; row <= BOARD__SIZE; row++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase]= true;
            }
        }
    }
    return hBase;
}

private int checkRowConstraint(boolean[][]coverBoard, int hBase) {
    for (int row = COVER__START__INDEX; row <= BOARD__SIZE; r++) {
        for (int n = COVER__START__INDEX; n <= BOARD__SIZE; n++, hBase++) {
            for (int column = COVER__START__INDEX; column <= BOARD__SIZE; column++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase]= true;
            }
        }
    }
    return hBase;
}

private int checkCellConstraint(boolean[][]coverBoard, int hBase) {
    for (int row = COVER__START__INDEX; row <= BOARD__SIZE; row++) {
        for (int column = COVER__START__INDEX; column <= BOARD__SIZE; column++, hBase++) {
            for (int n = COVER__START__INDEX; n <= BOARD__SIZE; n++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase]= true;
            }
        }
    }
    return hBase;
}

Ensuite, nous devons mettre à jour le tableau nouvellement créé avec notre structure de puzzle initiale:

private boolean[][]initializeExactCoverBoard(int[][]board) {
    boolean[][]coverBoard = createExactCoverBoard();
    for (int row = COVER__START__INDEX; row <= BOARD__SIZE; row++) {
        for (int column = COVER__START__INDEX; column <= BOARD__SIZE; column++) {
            int n = board[row - 1][column - 1];
            if (n != NO__VALUE) {
                for (int num = MIN__VALUE; num <= MAX__VALUE; num++) {
                    if (num != n) {
                        Arrays.fill(coverBoard[getIndex(row, column, num)], false);
                    }
                }
            }
        }
    }
    return coverBoard;
}

Nous sommes prêts à passer à la prochaine étape maintenant. Créons deux classes qui relieront nos cellules.

4.4. Noeud de danse

L’algorithme Dancing Links fonctionne sur une observation de base qui suit l’opération sur des listes de nœuds doublement liés:

node.prev.next = node.next
node.next.prev = node.prev

supprime le noeud, tandis que:

node.prev = node
node.next = node

restaure le noeud.

Chaque nœud de DLX est lié au nœud de gauche, de droite, de haut en bas.

La classe DancingNode aura toutes les opérations nécessaires pour ajouter ou supprimer des nœuds:

class DancingNode {
    DancingNode L, R, U, D;
    ColumnNode C;

    DancingNode hookDown(DancingNode node) {
        assert (this.C == node.C);
        node.D = this.D;
        node.D.U = node;
        node.U = this;
        this.D = node;
        return node;
    }

    DancingNode hookRight(DancingNode node) {
        node.R = this.R;
        node.R.L = node;
        node.L = this;
        this.R = node;
        return node;
    }

    void unlinkLR() {
        this.L.R = this.R;
        this.R.L = this.L;
    }

    void relinkLR() {
        this.L.R = this.R.L = this;
    }

    void unlinkUD() {
        this.U.D = this.D;
        this.D.U = this.U;
    }

    void relinkUD() {
        this.U.D = this.D.U = this;
    }

    DancingNode() {
        L = R = U = D = this;
    }

    DancingNode(ColumnNode c) {
        this();
        C = c;
    }
}

4.5. Noeud de colonne

ColumnNode class liera les colonnes entre elles:

class ColumnNode extends DancingNode {
    int size;
    String name;

    ColumnNode(String n) {
        super();
        size = 0;
        name = n;
        C = this;
    }

    void cover() {
        unlinkLR();
        for (DancingNode i = this.D; i != this; i = i.D) {
            for (DancingNode j = i.R; j != i; j = j.R) {
                j.unlinkUD();
                j.C.size--;
            }
        }
    }

    void uncover() {
        for (DancingNode i = this.U; i != this; i = i.U) {
            for (DancingNode j = i.L; j != i; j = j.L) {
                j.C.size++;
                j.relinkUD();
            }
        }
        relinkLR();
    }
}

4.6. Solveur

Ensuite, nous devons créer une grille composée de nos objets DancingNode et ColumnNode :

private ColumnNode makeDLXBoard(boolean[][]grid) {
    int COLS = grid[0].length;

    ColumnNode headerNode = new ColumnNode("header");
    List<ColumnNode> columnNodes = new ArrayList<>();

    for (int i = 0; i < COLS; i++) {
        ColumnNode n = new ColumnNode(Integer.toString(i));
        columnNodes.add(n);
        headerNode = (ColumnNode) headerNode.hookRight(n);
    }
    headerNode = headerNode.R.C;

    for (boolean[]aGrid : grid) {
        DancingNode prev = null;
        for (int j = 0; j < COLS; j++) {
            if (aGrid[j]) {
                ColumnNode col = columnNodes.get(j);
                DancingNode newNode = new DancingNode(col);
                if (prev == null) prev = newNode;
                col.U.hookDown(newNode);
                prev = prev.hookRight(newNode);
                col.size++;
            }
        }
    }

    headerNode.size = COLS;

    return headerNode;
}

Nous utiliserons la recherche heuristique pour trouver des colonnes et renvoyer un sous-ensemble de la matrice:

private ColumnNode selectColumnNodeHeuristic() {
    int min = Integer.MAX__VALUE;
    ColumnNode ret = null;
    for (
      ColumnNode c = (ColumnNode) header.R;
      c != header;
      c = (ColumnNode) c.R) {
        if (c.size < min) {
            min = c.size;
            ret = c;
        }
    }
    return ret;
}

Enfin, nous pouvons rechercher récursivement la réponse:

private void search(int k) {
    if (header.R == header) {
        handleSolution(answer);
    } else {
        ColumnNode c = selectColumnNodeHeuristic();
        c.cover();

        for (DancingNode r = c.D; r != c; r = r.D) {
            answer.add(r);

            for (DancingNode j = r.R; j != r; j = j.R) {
                j.C.cover();
            }

            search(k + 1);

            r = answer.remove(answer.size() - 1);
            c = r.C;

            for (DancingNode j = r.L; j != r; j = j.L) {
                j.C.uncover();
            }
        }
        c.uncover();
    }
}

S’il n’y a plus de colonnes, nous pouvons imprimer le tableau de Sudoku résolu.

5. Benchmarks

Nous pouvons comparer ces deux algorithmes différents en les exécutant sur le même ordinateur (nous évitons ainsi les différences de composants, la vitesse du processeur ou de la mémoire vive, etc.). Les temps réels seront différents d’un ordinateur à l’autre.

Cependant, nous devrions pouvoir voir les résultats relatifs, ce qui nous dira quel algorithme est plus rapide.

L’algorithme de retour en arrière nécessite environ 250 ms pour résoudre le problème.

Si nous comparons cela avec Dancing Links, qui prend environ 50 ms, nous pouvons voir un gagnant clair. Dancing Links est environ cinq fois plus rapide pour résoudre cet exemple particulier.

6. Conclusion

Dans ce didacticiel, nous avons présenté deux solutions à un sudoku avec Java. L’algorithme de retour en arrière, qui est un algorithme de force brute, peut résoudre facilement le puzzle standard 9 × 9.

L’algorithme Dancing Links, légèrement plus compliqué, a également été abordé. Les deux résolvent les énigmes les plus difficiles en quelques secondes.

Enfin, comme toujours, le code utilisé lors de la discussion peut être trouvé à l’adresse https://github.com/eugenp/tutorials/tree/master/algorithms-misc Miscellaneous-2[over sur GitHub].