Crie um Sudoku Solver em Java

Crie um Sudoku Solver em Java

*1. Visão geral *

Neste artigo, veremos os quebra-cabeças e algoritmos do Sudoku usados ​​para resolvê-lo.

Em seguida, implementaremos soluções em Java. A primeira solução será um simples ataque de força bruta. O segundo utilizará a técnica Links de dança.

Vamos ter em mente que o foco que vamos focar nos algoritmos e não no design do OOP.

===* 2. O quebra-cabeça do Sudoku *

Simplificando, o Sudoku é um quebra-cabeça de colocação combinatória de números com grade de 9 x 9 células parcialmente preenchida com números de 1 a 9. O objetivo é preencher os campos restantes em branco com o restante dos números para que cada linha e coluna tenha apenas um número de cada tipo.

Além disso, todas as subseções 3 x 3 da grade também não podem ter nenhum número duplicado. O nível de dificuldade aumenta naturalmente com o número de campos em branco em cada quadro.

====* 2.1 Placa de teste *

Para tornar nossa solução mais interessante e validar o algoritmo, usaremos o https://www.telegraph.co.uk/news/science/science-news/9359579/Worlds-hardest-sudoku-can-you -crack-it.html ["sudoku mais difícil do mundo"], que é:

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

====* 2.2 Placa resolvida *

E, para estragar a solução rapidamente - o quebra-cabeça resolvido corretamente nos dará o seguinte resultado:

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. Algoritmo de retrocesso *

====* 3.1 Introdução *

O algoritmo de retrocesso tenta resolver o quebra-cabeça testando cada célula em busca de uma solução válida.

Se não houver violação de restrições, o algoritmo passa para a próxima célula, preenche todas as soluções possíveis e repete todas as verificações.

Se houver uma violação, ele aumentará o valor da célula. Uma vez, o valor da célula atinge 9, e ainda há violação, o algoritmo volta para a célula anterior e aumenta o valor dessa célula.

Ele tenta todas as soluções possíveis.

====* 3.2 Solução *

Primeiro de tudo, vamos definir nosso quadro como uma matriz bidimensional de números inteiros. Usaremos 0 como nossa célula vazia.

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

Vamos criar um método solve () _ que usa o _board como parâmetro de entrada e itera através de linhas, colunas e valores testando cada célula para uma solução válida:

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

Outro método necessário é o método _isValid () _, que verificará as restrições do Sudoku, ou seja, verifique se a linha, a coluna e a grade 3 x 3 são válidas:

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

Essas três verificações são relativamente semelhantes. Primeiro, vamos começar com as verificações de linha:

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));
}

Em seguida, usamos código quase idêntico para validar a coluna:

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));
}

Além disso, precisamos validar a subseção 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;
}

Finalmente, precisamos de um método _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;
}

Uma vez, tudo o que é feito - o método isValid () _ pode simplesmente retornar _true.

Estamos quase prontos para testar a solução agora. O algoritmo está pronto. No entanto, ele retorna apenas true ou false.

Portanto, para verificar visualmente o quadro, precisamos apenas imprimir o resultado. Aparentemente, isso não faz parte do algoritmo.

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();
    }
}

Implementamos com sucesso o algoritmo de backtracking que resolve o quebra-cabeça do Sudoku!

Obviamente, há espaço para melhorias, pois o algoritmo verifica ingenuamente todas as combinações possíveis repetidas vezes (mesmo sabendo que a solução específica é inválida).

===* 4. Links de dança *

====* 4.1 Cobertura exata *

Vamos olhar para outra solução. O Sudoku pode ser descrito como um problema Capa exata, que pode ser representada pela matriz de incidência que mostra a relação entre dois objetos.

Por exemplo, se pegarmos números de 1 a 7 e a coleção de conjuntos _S = \ {A, B, C, D, E, F} _, em que:

  • A = \ {1, 4, 7}

  • B = \ {1, 4}

  • C = \ {4, 5, 7}

  • D = \ {3, 5, 6}

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

Nosso objetivo é selecionar esses subconjuntos que cada número existe apenas uma vez e nenhum é repetido, daí o nome.

Podemos representar o problema usando uma matriz, onde colunas são números e linhas são conjuntos:

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

A sub-coleção S* = \ {B, D, F} é a cobertura exata:

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

Cada coluna possui exatamente um 1 em todas as linhas selecionadas.

4.2 Algoritmo X

O algoritmo X é uma "abordagem de tentativa e erro" _ para encontrar todas as soluções para o problema exato de cobertura, ou seja, começando com nossa coleção de exemplos S = \ {A, B, C, D, E, F} _, localize a sub-coleção _S *= \ {B, D, F} .

O algoritmo X funciona da seguinte maneira:

  1. Se a matriz A não tiver colunas, a solução parcial atual é uma solução válida;
    terminar com êxito; caso contrário, escolha uma coluna c (deterministicamente)

  2. Escolha uma linha r de modo que A ~ r, c ~ = 1 (não-deterministicamente, isto é, tente todas as possibilidades)

  3. Inclua a linha r na solução parcial

  4. Para cada coluna j tal que A ~ r, j ~ = 1, para cada linha i tal que A ~ i, j ~ = 1,
    excluir linha i da matriz A e _ excluir coluna _j da matriz A

  5. Repita esse algoritmo recursivamente na matriz reduzida A

Uma implementação eficiente do algoritmo X é o algoritmo Links de dança (DLX abreviado) sugerido pelo Dr. Donald Knuth.

A solução a seguir foi fortemente inspirada pela this implementação em Java.

====* 4.3 Problema exato da tampa *

Primeiro, precisamos criar uma matriz que represente o quebra-cabeça do Sudoku como um problema de capa exata. A matriz terá 9 ^ 3 linhas, ou seja, uma linha para cada posição possível (9 linhas x 9 colunas) de cada número possível (9 números).

As colunas representarão o quadro (novamente 9 x 9) multiplicado pelo número de restrições.

Já definimos três restrições:

  • cada linha terá apenas um número de cada tipo

  • cada coluna terá apenas um número de cada tipo

  • cada subseção terá apenas um número de cada tipo

Além disso, há uma quarta restrição implícita:

*apenas um número pode estar em uma célula

Isso fornece quatro restrições no total e, portanto, 9 x 9 x 4 colunas na matriz Exact Cover:

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

Em seguida, precisamos atualizar o tabuleiro recém-criado com nosso layout inicial do quebra-cabeça:

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

Estamos prontos para avançar para a próxima etapa agora. Vamos criar duas classes que vincularão nossas células.

4.4 Nó Dançarino

O algoritmo Dancing Links opera em uma observação básica de que a seguinte operação em listas de nós duplamente vinculadas:

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

remove o nó, enquanto:

node.prev = node
node.next = node

restaura o nó.

Cada nó no DLX está vinculado ao nó à esquerda, direita, para cima e para baixo.

A classe DancingNode terá todas as operações necessárias para adicionar ou remover nós:

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 Nó da coluna

A classe ColumnNode vinculará colunas:

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 Solver

Em seguida, precisamos criar uma grade que consiste em nossos objetos DancingNode e 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;
}

Usaremos a pesquisa heurística para encontrar colunas e retornar um subconjunto da matriz:

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

Por fim, podemos procurar recursivamente a resposta:

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();
    }
}

Se não houver mais colunas, podemos imprimir o quadro de Sudoku resolvido.

*5. Benchmarks *

Podemos comparar esses dois algoritmos diferentes executando-os no mesmo computador (dessa forma, podemos evitar diferenças nos componentes, na velocidade da CPU ou na RAM, etc.). Os horários reais serão diferentes de computador para computador.

No entanto, poderemos ver resultados relativos, e isso nos dirá qual algoritmo é executado mais rapidamente.

O algoritmo de retrocesso leva cerca de 250ms para resolver o quadro.

Se compararmos isso com o Dancing Links, que leva cerca de 50ms, podemos ver um vencedor claro. Dancing Links é cerca de cinco vezes mais rápido ao resolver este exemplo em particular.

===* 6. Conclusão*

Neste tutorial, discutimos duas soluções para um quebra-cabeça sudoku com Java principal. O algoritmo de retorno, que é um algoritmo de força bruta, pode resolver o quebra-cabeça 9 × 9 padrão facilmente.

O algoritmo Dancing Links um pouco mais complicado também foi discutido. Ambos resolvem os quebra-cabeças mais difíceis em segundos.

Por fim, como sempre, o código usado durante a discussão pode ser encontrado https://github.com/eugenp/tutorials/tree/master/algorithms-misc Miscellaneous-2[over no GitHub].