数独,是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含1-9,不重复。
数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。
目标
对于一个给定的“残缺”的9 X 9棋盘,使用回溯法去给出一个解,如有解则打印出一个解;如果没有解,则输出没有找到相应的解法。
如:
给定一个“残缺”的棋盘,
-----------------------
| 3 | 1 7 | 2 8 |
| 5 | 2 | 9 4 |
| 6 2 | 9 | 1 7 |
-----------------------
| 3 | 4 1 | 6 |
| 4 7 | | 3 5 |
| 2 | 3 | 1 |
-----------------------
| 1 | 7 9 | 2 |
| 9 | 8 2 | 4 |
| 8 | 3 | 7 |
-----------------------
得到如下的一个结果。
-----------------------
| 3 9 4 | 5 1 7 | 6 2 8 |
| 5 1 7 | 6 2 8 | 3 9 4 |
| 6 2 8 | 3 9 4 | 5 1 7 |
-----------------------
| 9 3 5 | 4 7 1 | 2 8 6 |
| 4 7 1 | 2 8 6 | 9 3 5 |
| 2 8 6 | 9 3 5 | 4 7 1 |
-----------------------
| 1 4 3 | 7 5 9 | 8 6 2 |
| 7 5 9 | 8 6 2 | 1 4 3 |
| 8 6 2 | 1 4 3 | 7 5 9 |
-----------------------
思路
规则回顾
一个9X9的数独,其有三个规则:
- 每一行数字不能重复,1到9。
- 每一列数字不能重复,1到9。
- 每个宫(3X3)的块,数字不能重复,1到9。
解法
本文使用回溯法来解。
如何存储数独?
使用二维数组存储一个9 X 9的数独信息。其中,值为0表示该位置未放数值 (1-9)。
解决问题?
处理方向
从二维数组(int[][])的(0,0)坐标开始,先处理行数据,到该行最后时,从下一行的第一列开始处理下一行的数据。依此类推。
冲突判断
从上述描述的规则,我们已经知道,一个9 X9的数独有如下规则:
- 每一行数字不能重复,1到9。
- 每一列数字不能重复,1到9。
- 每个宫(3X3)的块,数字不能重复,1到9。
所以,我们尝试去填值的时候,需要判断是否符合规则,也即没有与其它数值冲突。
/**
* 在指定位置是否可以放置数据
*/
private boolean isSafe(int[][] grids, int row, int column, int value) {
return this.isColumnSafe(grids, column, value)
&& this.isRowSafe(grids, row, value)
&& this.isSmallBoxSafe(grids, row, column, value);
}
/**
* 某一行放置数据是否有冲突
*/
private boolean isRowSafe(int[][] grids, int row, int value) {
for (int i = 0; i < 9; i++) {
if (grids[row][i] == value) {
return false;
}
}
return true;
}
/**
* 某一列放置数据是否有冲突
*/
private boolean isColumnSafe(int[][] grids, int column, int value) {
for (int i = 0; i < 9; i++) {
if (grids[i][column] == value) {
return false;
}
}
return true;
}
/**
* 每个区域是 3 X 3 的子块,是否可以可以放置数据
*/
private boolean isSmallBoxSafe(int[][] grids, int row, int column, int value) {
int rowOffset = (row / 3) * 3;
int columnOffset = (column / 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (grids[rowOffset + i][columnOffset + j] == value) {
return false;
}
}
}
return true;
}
结束条件?
一个数独的解法,其每个位置的数值,都符合上述安全的规则。所以,最简单的方法是循环遍历二维数组中的数值,然后判断每个数值是否都是安全的,且没有不为0的数值。
如果有多次这样的判断调用,那判断的成本太大了。我们可以使用更加简单的方法判断。
因为,我们的做法是先处理行,然后处理列。如果某行某列的值为0,则填充一个合适的值之后,处理下一列;或者某行某列的数值不为0,直接处理下一列。所以,如果找到一种解法,最后的条件可以根据最终行和列的值来判断,row为8,column为9。
结束条件如下:
/**
* 解数独
*/
private boolean solve(int[][] grids, int row, int column) {
/**
* 结束条件
*/
if (row == 8 && column == 9) {
return true;
}
//省略其它
}
代码
有了上述的思路,就很容易写出代码来了。如:
/**
*
* @author wangmengjun
*
*/
public class SudokuPuzzleSolver {
/**
* 解数独,并打印结果
*/
public void solve(int[][] grids) {
printArray(grids);
System.out.println();
if (this.solve(grids, 0, 0)) {
printArray(grids);
} else {
System.out.println("没有找到相应的解法");
}
}
/**
* 解数独
*/
private boolean solve(int[][] grids, int row, int column) {
/**
* 结束条件
*/
if (row == 8 && column == 9) {
return true;
}
/**
* 如果column == 9, 那么需要换行,也就需要更新column和row的值.
*/
if (column == 9) {
column = 0;
row++;
}
/**
* 如果当前位置上grids[row][column]的值不为0,则处理下一个
*/
if (grids[row][column] != 0) {
return solve(grids, row, column + 1);
}
/**
* 如果当前位置上grids[row][column]的值为0, 尝试在1~9的数字中选择合适的数字
*/
for (int num = 1; num <= 9; num++) {
if (isSafe(grids, row, column, num)) {
grids[row][column] = num;
if (solve(grids, row, column + 1)) {
return true;
}
}
}
/**
* 回溯重置
*/
grids[row][column] = 0;
return false;
}
/**
* 某一行放置数据是否有冲突
*/
private boolean isRowSafe(int[][] grids, int row, int value) {
for (int i = 0; i < 9; i++) {
if (grids[row][i] == value) {
return false;
}
}
return true;
}
/**
* 某一列放置数据是否有冲突
*/
private boolean isColumnSafe(int[][] grids, int column, int value) {
for (int i = 0; i < 9; i++) {
if (grids[i][column] == value) {
return false;
}
}
return true;
}
/**
* 每个区域是 3 X 3 的子块,是否可以可以放置数据
*/
private boolean isSmallBoxSafe(int[][] grids, int row, int column, int value) {
int rowOffset = (row / 3) * 3;
int columnOffset = (column / 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (grids[rowOffset + i][columnOffset + j] == value) {
return false;
}
}
}
return true;
}
/**
* 在指定位置是否可以放置数据
*/
private boolean isSafe(int[][] grids, int row, int column, int value) {
return this.isColumnSafe(grids, column, value)
&& this.isRowSafe(grids, row, value)
&& this.isSmallBoxSafe(grids, row, column, value);
}
/**
* 打印二维数组到控制台
*/
private void printArray(int[][] grids) {
for (int i = 0; i < 9; i++) {
if (i % 3 == 0) {
System.out.println(" -----------------------");
}
for (int j = 0; j < 9; j++) {
if (j % 3 == 0) {
System.out.print("| ");
}
System.out.print(grids[i][j] == 0 ? " " : grids[i][j]);
System.out.print(" ");
}
System.out.println("|");
}
System.out.println(" -----------------------");
}
}
测试
完全空白的数独
测试代码和输出如下:
import java.util.Random;
public class Main {
public static void main(String[] args) {
SudokuPuzzleSolver example = new SudokuPuzzleSolver();
int[][] grids = new int[9][9];
example.solve(grids);
}
}
-----------------------
| | | |
| | | |
| | | |
-----------------------
| | | |
| | | |
| | | |
-----------------------
| | | |
| | | |
| | | |
-----------------------
-----------------------
| 1 2 3 | 4 5 6 | 7 8 9 |
| 4 5 6 | 7 8 9 | 1 2 3 |
| 7 8 9 | 1 2 3 | 4 5 6 |
-----------------------
| 2 1 4 | 3 6 5 | 8 9 7 |
| 3 6 5 | 8 9 7 | 2 1 4 |
| 8 9 7 | 2 1 4 | 3 6 5 |
-----------------------
| 5 3 1 | 6 4 2 | 9 7 8 |
| 6 4 2 | 9 7 8 | 5 3 1 |
| 9 7 8 | 5 3 1 | 6 4 2 |
-----------------------
部分残缺的数独
如果是完全空白的场景,那么结果只会有一种。
如果想让产生的数独有随机性,我们可以随机初始化第一行的数据,如:
import java.util.Random;
public class Main {
public static void main(String[] args) {
SudokuPuzzleSolver example = new SudokuPuzzleSolver();
int[][] grids = initGrids();
example.solve(grids);
}
public static int[][] initGrids() {
int[][] grids = new int[9][9];
int[] firstRow = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Random rand = new Random();
for (int i = 0; i < 10; i++) {
int randIndex = rand.nextInt(8) + 1;
int temp = firstRow[0];
firstRow[0] = firstRow[randIndex];
firstRow[randIndex] = temp;
}
grids[0] = firstRow;
return grids;
}
}
某次运行的结果如下:
-----------------------
| 3 8 1 | 2 4 6 | 7 5 9 |
| | | |
| | | |
-----------------------
| | | |
| | | |
| | | |
-----------------------
| | | |
| | | |
| | | |
-----------------------
-----------------------
| 3 8 1 | 2 4 6 | 7 5 9 |
| 2 4 5 | 1 7 9 | 3 6 8 |
| 6 7 9 | 3 5 8 | 1 2 4 |
-----------------------
| 1 2 3 | 4 6 5 | 8 9 7 |
| 4 5 7 | 8 9 1 | 2 3 6 |
| 8 9 6 | 7 2 3 | 4 1 5 |
-----------------------
| 5 1 2 | 6 8 4 | 9 7 3 |
| 7 6 4 | 9 3 2 | 5 8 1 |
| 9 3 8 | 5 1 7 | 6 4 2 |
-----------------------
如果已经有一个数独难题,想给出一个解法,则可以使用如下方式:
public class Main {
public static void main(String[] args) {
SudokuPuzzleSolver example = new SudokuPuzzleSolver();
int[][] grids = initGrids();
example.solve(grids);
}
public static int[][] initGrids() {
int[][] grids = { { 3, 0, 0, 0, 1, 7, 0, 2, 8 },
{ 5, 0, 0, 0, 2, 0, 0, 9, 4 }, { 6, 2, 0, 0, 9, 0, 0, 1, 7 },
{ 0, 3, 0, 4, 0, 1, 0, 0, 6 }, { 4, 7, 0, 0, 0, 0, 0, 3, 5 },
{ 2, 0, 0, 0, 3, 0, 0, 0, 1 }, { 1, 0, 0, 7, 0, 9, 0, 0, 2 },
{ 0, 0, 9, 8, 0, 2, 0, 4, 0 }, { 8, 0, 0, 0, 0, 3, 7, 0, 0 } };
return grids;
}
}
运行结果如下:
-----------------------
| 3 | 1 7 | 2 8 |
| 5 | 2 | 9 4 |
| 6 2 | 9 | 1 7 |
-----------------------
| 3 | 4 1 | 6 |
| 4 7 | | 3 5 |
| 2 | 3 | 1 |
-----------------------
| 1 | 7 9 | 2 |
| 9 | 8 2 | 4 |
| 8 | 3 | 7 |
-----------------------
-----------------------
| 3 9 4 | 5 1 7 | 6 2 8 |
| 5 1 7 | 6 2 8 | 3 9 4 |
| 6 2 8 | 3 9 4 | 5 1 7 |
-----------------------
| 9 3 5 | 4 7 1 | 2 8 6 |
| 4 7 1 | 2 8 6 | 9 3 5 |
| 2 8 6 | 9 3 5 | 4 7 1 |
-----------------------
| 1 4 3 | 7 5 9 | 8 6 2 |
| 7 5 9 | 8 6 2 | 1 4 3 |
| 8 6 2 | 1 4 3 | 7 5 9 |
-----------------------
类似题目
通过阅读本文,大家可以掌握使用回溯法解数独的方法。
其实,使用回溯法可以去解决较多的问题,比如:比较典型的是八皇后问题。
有兴趣的读者可以尝试编写一下。