数独游戏的难度等级分析及求解算法研究3——数独解法研究及附录
3 数独解法研究
数独的游戏规则十分简单,在一个9×9的单元格组成的表中,填入数字1~9,每个数字在每个单元中只能出现一次解答数独。人脑求解数独是最为普遍的方法,思考数独求解的过程,可以活跃思维,给思维带来无穷的乐趣。人工求解数独的过程,需要用到很多技巧,有列排除法,行排除法,行列排除法,唯一性法,单元格对法等技巧。而这些技巧都是建立在遵守约束条件的基础上发明创造的。计算机语言求解数独的过程,是一个模拟人脑思考数独的过程,这个过程程序化高,准确率更高,思考时间更短。论文主要使用递归法和回溯法求解数独。
3.1 求解约束条件
无论是人工求解还是计算机求解,都遵循一个共同的规律,就是计算机在9×9的空格中填满1-9的数字,要求大正方形每一行、每一列及每个九宫格内均必须包括1到9的每一个数字,不重复也不遗漏。从数独的规则,得出解决数独游戏必须遵守的约束条件:
(1)每一格的数值范围仅限于1-9;
(2)每一格内的数字在当前行不允许重复;
(3)每一格的数字在当前行不允许重复;
(4)每一格的数字在当前小九宫内不允许重复。
3.2 递归求解数独过程
3.2.1 递归算法
递归算法,在函数或子过程的内部,直接或者间接地调用自己的算法。求解数独算法的递归公式为求解每一个数独表格中的空格,空格填写完整则递归结束。递归算法求解数独的详细程序见附录3:
1、读取数独题目文本;
2、将数独题目转换为数独网格;
3、创建动态数组ArrayList存放数独网格;
4、递归法求解数独题目:
(1) 依次读取数独题目中空格;
(2) 根据空格的位置确定空格所在的行、列与九宫格,并依次填入1-9的数字,检查是否符合数独解题的约束条件,如不符合要求,则清除单元格的数字,尝试填写另一个1-9的数字;
(3) 当所填入的数字符合约束条件时,使用递归算法,调用同样的求解方法,再读取下一个空格,填写此空格的数字。依次进行,直到空格全部填写。
5、求解后的数组存放于动态数组中,输出求解结果。
3.2.2 递归算法的流程图
递归算法求解数独的流程如上小节所示,详细的流程图如图7所示,具体代码可以查看附录3。
图7 递归法求解数独流程图
Flow chart of using recursive algorithm to solve sudoku
3.3 回溯解法
3.3.1 回溯法程序设计过程
回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。回溯法求解数独的原理,采用试探的方法填数,在填数的过程中,当发现所填的答案不能得到正确的解答时,取消上一步甚至是上几步的计算,再尝试其他数字寻找问题的答案。
回溯算法的过程如下:
1、读取题目;
2、检查题目的合法性;
3、逐个取得空白处;
4、根据数独题目解答的约束条件,有序递推,若可以填入数字则填入数字,并入栈以便回溯,否则回溯至前面填入数字处重新填数;
5、将填好的数字组成数组,并输出。
3.3.2 回溯算法流程图
回溯算法求解数独的流程如上一小节所描述,详细的流程图如图8所示,具体的代码实现可以参考附录4所示。
图8 回溯算法求解数独流程图
Flow chart of using backtracking algorithm to solve sudoku
3.4 算法对比与分析
回溯算法其实是一种试探,该方法放弃关于问题规模大小的限制,并将问题的方案按某种顺序逐一枚举和试验。发现当前方案不可能有解时,就选择下一个方案,倘若当前方案不满足问题的要求时,继续扩大当前方案的规模,并继续试探。如果当前方案满足所有要求时,该方案就是问题的一个解。放弃当前方案,寻找下一个方案的过程称为回溯。而递归算法依赖于前一步的结果,它的结果来源于一条主线,是确定的,而不是试探的结果,这就是其与回溯的区别。
递归算法的原理是候选数法,先建立数独网格中每个空格的候选数列表1-9,根据游戏规则的约束条件和谜题的已知条件,逐步清除各个宫格候选数的不可能取值的候选数,最终达到解题的目的。回溯法则是使用试探法进行求解,将在数独的每个空格中依次试着填入1-9的数字,检查是否符合各种约束条件。
对比两种算法的过程,我们可以看出,递归算法的效率往往很低, 费时和费内存空间. 但是递归也有其长处, 它能使一个蕴含递归关系且结构复杂的程序简介精炼, 增加可读性.递归求解数独的过程,思路清晰,结构简单,而且,如果数独题目有多解,可以解出多个解。回溯法是一个非递归的过程,效率较高,但对于多解的数独谜题,它只可以得出单解。
附录:代码
附录1计算空格数
package com.qing.sudoku.blank;
public class CountBlankNum {
public CountBlankNum() {
sudoku = "006000000031800000000000056000000000200010007000008040000000002000005310000200500";
num = 0;
}
/*** 计空格数 */
public int countBlank(String sudoku) {
this.sudoku = sudoku;
String[] ary = ("," + sudoku + ",").split("0");
num = ary.length - 1;
return num;
}
/** * 根据空格数划分难度*/
public int countLevel(int BlankNum) {
int level;
if (BlankNum < 46 && BlankNum > 39) {
level = 1;
} else if (BlankNum < 51 && BlankNum > 45) {
level = 2;
} else if (BlankNum < 56 && BlankNum > 50) {
level = 3;
} else if (BlankNum < 61 && BlankNum > 55) {
level = 4;
} else if (BlankNum < 66 && BlankNum > 60) {
level = 5;
} else {
level = 0;
}
if (level == 0) {
System.out.println("Sorry,你输入的数独的空格数:" + BlankNum + ",这超出我们的处理范围");
} else {
System.out.println("空格数:" + BlankNum);
System.out.println("难度等级:" + level + "级");}
return level;}
public String getSudoku() {
return sudoku;
}
public String sudoku;
public int num;
/** 主函数*/
public static void main(String[] args) {
CountBlankNum count = new CountBlankNum();
String su;
int blackNum;
int level;
su = count.getSudoku();
blackNum = count.countBlank(su);
level = count.countLevel(blackNum);
}}
附录2计算空格自由度
package com.qing.sudoku.free;
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
public class CountFree extends JFrame {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
CountFree frame = new CountFree();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}});}
public CountFree() {
setTitle("难度系数计算");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
buttonPanel = new JPanel();
txtPanel = new JPanel();
/** 输入数独题目*/
JButton sudokuProBtn = new JButton("输入数独");
buttonPanel.add(sudokuProBtn);
sudokuProBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
sudokuStr = JOptionPane.showInputDialog("请输入你想计算难度的数独题目:");
}});
/*** 计算数独的空格自由度*/
JButton freeBtn = new JButton("空格自由度");
buttonPanel.add(freeBtn);
freeBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// sudokuStr = "600000004050009800000003570003060280802094000000000000080042000001000020500010008";
lab.setText("空格自由度:");
countFree(sudokuStr);
}});
/** 计算数独难度系数*/
JButton sudokuLevelBtn = new JButton("难度系数");
buttonPanel.add(sudokuLevelBtn);
sudokuLevelBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
lab.setText("难度系数:");
countLevel();
}});
/** 标签显示文本*/
lab = new JLabel();
txtLab = new JLabel();
txtPanel.add(lab);
txtPanel.add(txtLab);
add(buttonPanel, BorderLayout.NORTH);
add(txtPanel, BorderLayout.CENTER);
}
/* * 计算空格自由度*/
public int countFree(String str) {
strExArray(str);
int countXY = lineRowFree(b);
int count = nineFree(b);
freeLevel = countXY + count;
String s = String.valueOf(freeLevel);
txtLab.setText(s);
return freeLevel;
}
/** 计算难度系数*/
public int countLevel() {
// sudokuStr = "600000004050009800000003570003060280802094000000000000080042000001000020500010008";
int free = countFree(sudokuStr);
int sudokuLevel;
if (free > 0 && free < 721) {
sudokuLevel = 1;
} else if (free > 720 && free < 841) {
sudokuLevel = 2;
} else if (free > 840 && free < 961) {
sudokuLevel = 3;
} else if (free > 960 && free < 1081) {
sudokuLevel = 4;
} else if (free > 1080 && free < 2106) {
sudokuLevel = 5;
} else {
sudokuLevel = 0;
}
if (sudokuLevel == 0) {
System.out.println("空格数超出了考虑的范围");
} else {
String s = String.valueOf(sudokuLevel);
txtLab.setText(s);}
return sudokuLevel;}
/* * 将字符串转换为二维数组*/
public void strExArray(String str) {
char a[] = str.toCharArray();
b = new char[9][9];
int k = -1;
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
k++;
b[i][j] = a[k];}}}
/*** 计算行列自由度的和*/
public int lineRowFree(char[][] c) {
int countX = 0;
int countY = 0;
for (int i = 0; i < c.length; i++) {
for (int j = 0; j < 9; j++) {
if (b[i][j] == '0') {
int row = i;
int line = j;
for (int r = 0; r < 9; r++) {
if (b[row][r] == '0') {
countX++;}
if (b[r][line] == '0') {
countY++;}}}}}
return countX + countY;
}
/** 接着计算九宫格的自由度*/
public int nineFree(char[][] c) {
int p = 0;
int r = 0;
int m = 9;
int n = 9;
int countZ = 0;
int countH = 0;
// 计算出为0的格数
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (c[i][j] == '0') {
countZ++;}}}
// 计算九宫格内的空格数
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (c[i][j] == '0') {
int row = i % 3;
int line = j % 3;
if (row == 1 && line == 1) {
for (p = i - 1; p < i + 2; p++) {
for (r = j - 1; r < j + 2; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 1 && line == 2) {
for (p = i - 1; p < i + 2; p++) {
for (r = j - 2; r < j + 1; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 1 && line == 0) {
for (p = i - 1; p < i + 2; p++) {
for (r = j; r < j + 3; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 2 && line == 1) {
for (p = i - 2; p < i + 1; p++) {
for (r = j - 1; r < j + 2; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 2 && line == 2) {
for (p = i - 2; p < i + 1; p++) {
for (r = j - 2; r < j + 1; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 2 && line == 0) {
for (p = i - 2; p < i + 1; p++) {
for (r = j; r < j + 3; r++) {
if (c[p][r] == '0') {
countH++;}}}
} else if (row == 0 && line == 1) {
for (p = i; p < i + 3; p++) {
for (r = j - 1; r < j + 2; r++) {
if (c[p][r] == '0') {
countH++;
}}}
} else if (row == 0 && line == 2) {
for (p = i; p < i + 3; p++) {
for (r = j - 2; r < j + 1; r++) {
if (c[p][r] == '0') {
countH++;
}}}} else if (row == 0 && line == 0) {
for (p = i; p < i + 3; p++) {
for (r = j; r < j + 3; r++) {
if (c[p][r] == '0') {
countH++;
}}}} else {
}} else {
}}}
return (countH - countZ);
}
private String sudokuStr = null;
private JPanel buttonPanel;
private JPanel txtPanel;
private JLabel txtLab;
private JLabel lab;
private int num;
private int freeLevel;
private char[][] b;
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 150;
}
附录3递归法求解数独的程序
此程序中有两个类,一个是SudokuSolving.java,这个是主函数。而SudokuGrid.java则是表示数独的属性类,类中包含了函数执行的基本方法,如逐个读取空格和查找空白,判读所填数字是否符合各种条件。
SudokuSolving.java
package SudokuSolver;
import java.io.*;
import java.util.*;
public class SudokuSolving {
public static void main(String[] args) throws Exception {
String path = "sudoku.txt";
FileReader rd = new FileReader(path);
while(true){
SudokuGrid grid = SudokuGrid.ReadGrid(rd);
if(grid == null){
break;
}
List<SudokuGrid> solutions = new ArrayList<SudokuGrid>();
solve(grid, solutions);
printSolutions(grid,solutions);
}
}
private static void solve(SudokuGrid grid, List<SudokuGrid> solutions) {
if (solutions.size() >= 2) {
return;
}
int loc = grid.findEmptyCell();
if (loc < 0) {
solutions.add(grid.clone());
return;
}
for (int n=1; n<10; n++){
if (grid.set(loc, n)) {
solve(grid, solutions);
grid.clear(loc);
}
}
}
private static void printSolutions(SudokuGrid grid, List<SudokuGrid> solutions) {
System.out.println("Original");
System.out.println(grid);
if (solutions.size() == 0) {
System.out.println("Unsolveable");
} else if (solutions.size() == 1) {
System.out.println("Solved");
} else {
System.out.println("At least two solutions");
}
for (int i=0; i<solutions.size(); i++) {
System.out.println(solutions.get(i));
}
System.out.println();
System.out.println();
}
}
SudokuGrid.java
package SudokuSolver;
import java.io.*;
public class SudokuGrid implements Cloneable {
int[] cells = new int[81];
int[] columns = new int[9];
int[] rows = new int[9];
int[] boxes = new int[9];
public static SudokuGrid ReadGrid(Reader stream)throws Exception{
SudokuGrid grid = new SudokuGrid();
for(int loc=0;loc<grid.cells.length;){
//逐个读取字符
int ch = stream.read();
if(ch < 0){
return null;
}
//字符#是用于评论;随后字符将被忽略,直到遇到换行符.
//这个地方不理解
if(ch == '#'){
if (ch >= 0 && ch != '\n' && ch != '\r') {
ch = stream.read();
}
}else if(ch >= '1' && ch <= '9'){
grid.set(loc, ch-'0');
loc++;
}else if(ch == '.' || ch == '0'){
loc++;
}
}
return grid;
}
// 根据单元格的位置,尝试填入数字,如果符合要求则返回true,如果不符合要求则返回false
boolean set(int loc, int num) {
int r = loc/9;
int c = loc%9;
int blockLoc = (r/3)*3+c/3;
//检查是否有相同的单元格,如果有的话,则返回false
boolean canSet = (cells[loc] == 0
&& (columns[c] & (1<<num)) == 0
&& (rows[r] & (1<<num)) == 0
&& (boxes[blockLoc] & (1<<num)) == 0);
if (!canSet) {
return false;
}
cells[loc] = num;
columns[c] |= (1<<num);
rows[r] |= (1<<num);
boxes[blockLoc] |= (1<<num);
return true;
}
//寻找空白处
public int findEmptyCell() {
for (int i=0; i<cells.length; i++) {
if (cells[i] == 0) {
return i;}}
return -1;}
public void clear(int loc) {
int r = loc/9;
int c = loc%9;
int blockLoc = (r/3)*3+c/3;
int num = cells[loc];
cells[loc] = 0;
columns[c] ^= (1<<num);
rows[r] ^= (1<<num);
boxes[blockLoc] ^= (1<<num);}
//复制数独网格
public SudokuGrid clone() {
SudokuGrid grid = new SudokuGrid();
grid.cells = cells.clone();
grid.columns = columns.clone();
grid.rows = rows.clone();
grid.boxes = boxes.clone();
return grid;
}
//设置输出格式
public String toString() {
StringBuffer buf = new StringBuffer();
for (int r=0; r<9; r<span style="color: