Create a Sudoku Solver in Java – 在Java中创建一个数独解算器

最后修改: 2018年 2月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’re going to look at Sudoku puzzle and algorithms used for solving it.

在这篇文章中,我们要看一下数独谜题和用于解题的算法。

Next, we’ll implement solutions in Java. The first solution will be a simple brute-force attack. The second will utilize the Dancing Links technique.

接下来,我们将在Java中实现解决方案。第一个解决方案将是一个简单的暴力攻击。第二种将利用Dancing Links技术。

Let’s keep in mind that the focus we’re going to focus on the algorithms and not on the OOP design.

让我们牢记,我们要关注的重点是算法,而不是OOP设计。

2. The Sudoku Puzzle

2.数独之谜

Simply put, Sudoku is a combinatorial number placement puzzle with 9 x 9 cell grid partially filled in with numbers from 1 to 9. The goal is to fill remaining, blank fields with the rest of numbers so that each row and column will have only one number of each kind.

简单地说,数独是一种组合式数字摆放谜题,在9×9的单元格中部分填入1-9的数字。游戏的目标是将剩余的空白区域填上其余的数字,使每一行和每一列都只有一个数字。

What’s more, every 3 x 3 subsection of the grid can’t have any number duplicated as well. The level of difficulty naturally rises with the number of blank fields in each board.

更重要的是,每一个3×3的格子都不能有任何数字重复。难度自然会随着每个棋盘中空白区域的数量增加而上升。

2.1. Test Board

2.1.测试板

To make our solution more interesting and to validate the algorithm, we’re going to use the “world’s hardest sudoku” board, which is:

为了使我们的解决方案更加有趣,并验证该算法,我们将使用“世界上最难的数独”棋盘,也就是。

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

2.2. Solved Board

2.2.已解决的板块

And, to spoil the solution quickly – the correctly solved puzzle will give us the following result:

而且,为了快速破坏解决方案–正确解出的谜题将给我们带来以下结果。

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. Backtracking Algorithm

3.回溯算法

3.1. Introduction

3.1.简介

Backtracking algorithm tries to solve the puzzle by testing each cell for a valid solution.

Backtracking算法试图通过测试每个单元格的有效解来解开谜题。

If there’s no violation of constraints, the algorithm moves to the next cell, fills in all potential solutions and repeats all checks.

如果没有违反约束,算法就会移动到下一个单元格,填入所有潜在的解决方案,并重复所有检查。

If there’s a violation, then it increments the cell value. Once, the value of the cell reaches 9, and there is still violation then the algorithm moves back to the previous cell and increases the value of that cell.

如果有违反,那么它就会增加单元格的值。一旦单元格的值达到9,并且仍然有违规行为,那么算法就会回到上一个单元格并增加该单元格的值。

It tries all possible solutions.

它尝试了所有可能的解决方案。

3.2. Solution

3.2.解决方案

First of all, let’s define our board as a two-dimensional array of integers. We will use 0 as our empty cell.

首先,让我们把我们的棋盘定义为一个整数的二维数组。我们将使用0作为我们的空单元。

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

Let’s create a solve() method that takes the board as the input parameter and iterates through rows, columns and values testing each cell for a valid solution:

让我们创建一个solve()方法,将board作为输入参数,通过行、列和值的迭代,测试每个单元格的有效解决方案。

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

Another method that we needed is isValid() method, which is going to check Sudoku constraints, i.e., check if the row, column, and 3 x 3 grid are valid:

我们需要的另一个方法是isValid()方法,它要检查数独约束,即检查行、列和3 x 3网格是否有效。

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

These three checks are relatively similar. First, let’s start with row checks:

这三种检查是比较类似的。首先,让我们从行检查开始。

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

Next, we use almost identical code to validate column:

接下来,我们使用几乎相同的代码来验证列。

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

Furthermore, we need to validate 3 x 3 subsection:

此外,我们需要验证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;
}

Finally, we need a checkConstraint() method:

最后,我们需要一个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;
}

Once, all that is done isValid() method can simply return true.

一旦,所有这些都完成了isValid()方法可以简单地返回true

We’re almost ready to test the solution now. The algorithm is done. However, it returns true or false only.

我们现在几乎准备好测试解决方案了。该算法已经完成。然而,它只返回truefalse

Therefore, to visually check the board we need just to print out the result. Apparently, this isn’t part of the algorithm.

因此,为了直观地检查棋盘,我们只需要把结果打印出来。显然,这并不是算法的一部分。

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

We’ve successfully implemented backtracking algorithm that solves the Sudoku puzzle!

我们已经成功地实现了回溯算法,解决了数独谜题!

Obviously, there’s room for improvements as the algorithm naively checks each possible combinations over and over again (even though we know the particular solution is invalid).

显然,还有改进的余地,因为该算法天真地一遍又一遍地检查每个可能的组合(即使我们知道特定的解决方案是无效的)。

4. Dancing Links

4.舞蹈链接

4.1. Exact Cover

4.1.确切的封面

Let’s look at another solution. Sudoku can be described as an Exact Cover problem, which can be represented by incidence matrix showing the relationship between two objects.

让我们来看看另一种解决方案。数独可以说是一个精确覆盖问题,它可以用显示两个物体之间关系的入射矩阵来表示。

For example, if we take numbers from 1 to 7 and the collection of sets S = {A, B, C, D, E, F}, where:

例如,如果我们从1到7的数字和集合的集合S = {A, B, C, D, E, F},其中。

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

Our goal is to select such subsets that each number is there only once and none is repeated, hence the name.

我们的目标是选择这样的子集,每个数字只出现一次,没有重复的,所以叫这个名字。

We can represent the problem using a matrix, where columns are numbers and rows are sets:

我们可以用一个矩阵来表示这个问题,其中列是数字,行是集合。

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

Subcollection S* = {B, D, F} is exact cover:

子集合S*={B,D,F}是精确覆盖。

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

Each column has exactly one 1 in all selected rows.

在所有选定的行中,每一列正好有一个1。

4.2. Algorithm X

4.2.算法X

Algorithm X is a “trial-and-error approach” to find all solutions to the exact cover problem, i.e. starting with our example collection S = {A, B, C, D, E, F}, find subcollection S* = {B, D, F}.

算法X是一种“试错法”,以找到精确覆盖问题的所有解决方案,即从我们的例子集合S = {A, B, C, D, E, F}开始,找到子集合S* = {B, D, F}。

Algorithm X works as follows:

算法X的工作原理如下。

  1. If the matrix A has no columns, the current partial solution is a valid solution;
    terminate successfully, otherwise, choose a column c (deterministically)
  2. Choose a row r such that Ar, c = 1 (nondeterministically, i.e., try all possibilities)
  3. Include row r in the partial solution
  4. For each column j such that Ar, j = 1, for each row i such that Ai, j = 1,
    delete row i from matrix A and delete column j from matrix A
  5. Repeat this algorithm recursively on the reduced matrix A

An efficient implementation of the Algorithm X is Dancing Links algorithm (DLX for short) suggested by Dr. Donald Knuth.

算法X的一个有效实现是Dancing Links算法(简称DLX),该算法由Donald Knuth博士建议。

The following solution has been heavily inspired by this Java implementation.

下面的解决方案在很大程度上受到这个Java实现的启发。

4.3. Exact Cover Problem

4.3.精确覆盖问题

First, we need to create a matrix that will represent Sudoku puzzle as an Exact Cover problem. The matrix will have 9^3 rows, i.e., one row for every single possible position (9 rows x 9 columns) of every possible number (9 numbers).

首先,我们需要创建一个矩阵,将数独谜题表现为一个精确覆盖问题。矩阵将有9^3行,也就是说,每一个可能的数字(9行x9列)都有一行。

Columns will represent the board (again 9 x 9) multiplied by the number of constraints.

列将代表棋盘(同样是9 x 9)乘以制约因素的数量。

We’ve already defined three constraints:

我们已经定义了三个约束。

  • each row will have only one number of each kind
  • each column will have only one number of each kind
  • each subsection will have only one number of each kind

Additionally, there is implicit fourth constraint:

此外,还有隐含的第四个约束。

  • only one number can be in a cell

This gives four constraints in total and therefore 9 x 9 x 4 columns in the Exact Cover matrix:

这样一来,总共有四个约束条件,因此在精确覆盖矩阵中有9×9×4列。

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

Next, we need to update the newly created board with our initial puzzle layout:

接下来,我们需要用我们最初的拼图布局来更新新创建的棋盘。

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

We are ready to move to the next stage now. Let’s create two classes that will link our cells together.

我们现在已经准备好进入下一个阶段了。让我们创建两个类,将我们的单元格连接起来。

4.4. Dancing Node

4.4.跳舞的节点

Dancing Links algorithm operates on a basic observation that following operation on doubly linked lists of nodes:

Dancing Links算法的操作是基于一个基本的观察,即对节点的双链表进行以下操作。

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

removes the node, while:

移除该节点,而。

node.prev = node
node.next = node

restores the node.

恢复该节点。

Each node in DLX is linked to the node on the left, right, up and down.

DLX中的每个节点都与左、右、上、下的节点相连。

DancingNode class will have all operations needed to add or remove nodes:

DancingNode类将拥有添加或删除节点所需的所有操作。

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. Column Node

4.5.柱状节点

ColumnNode class will link columns together:

ColumnNode类将把列连接起来。

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

4.6.求解器

Next, we need to create a grid consisting of our DancingNode and ColumnNode objects:

接下来,我们需要创建一个由DancingNodeColumnNode对象组成的网格。

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

We’ll use heuristic search to find columns and return a subset of the matrix:

我们将使用启发式搜索来寻找列并返回矩阵的一个子集。

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

Finally, we can recursively search for the answer:

最后,我们可以递归地寻找答案。

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

If there are no more columns, then we can print out the solved Sudoku board.

如果没有更多的列,那么我们可以打印出已解决的数独棋盘。

5. Benchmarks

5.基准

We can compare those two different algorithms by running them on the same computer (this way we can avoid differences in components, the speed of CPU or RAM, etc.). The actual times will differ from computer to computer.

我们可以通过在同一台电脑上运行这两种不同的算法来进行比较(这样我们可以避免部件的差异,CPU或RAM的速度等)。实际时间在不同的电脑上会有所不同。

However, we should be able to see relative results, and this will tell us which algorithm runs faster.

然而,我们应该能够看到相对的结果,这将告诉我们哪个算法运行得更快。

The Backtracking Algorithm takes around 250ms to solve the board.

回溯算法需要大约250ms来解决棋盘。

If we compare this with the Dancing Links, which takes around 50ms, we can see a clear winner. Dancing Links is around five times faster when solving this particular example.

如果我们将其与Dancing Links进行比较,后者需要大约50ms,我们可以看到一个明显的赢家。在解决这个特定的例子时,Dancing Links的速度大约是5倍。

6. Conclusion

6.结论

In this tutorial, we’ve discussed two solutions to a sudoku puzzle with core Java. The backtracking algorithm, which is a brute-force algorithm, can solve the standard 9×9 puzzle easily.

在本教程中,我们讨论了用核心Java解决一个数独谜题的两种方法。回溯算法是一种暴力算法,可以轻松地解决标准的9×9谜题。

The slightly more complicated Dancing Links algorithm has been discussed as well. Both solve the hardest puzzles within seconds.

稍微复杂的Dancing Links算法也被讨论过。两者都能在几秒钟内解决最难的谜题。

Finally, as always, the code used during the discussion can be found over on GitHub.

最后,像往常一样,讨论中使用的代码可以在GitHub上找到