数独游戏的难度等级分析及求解算法研究3——数独解法研究及附录


3 数独解法研究

数独的游戏规则十分简单,在一个9×9的单元格组成的表中,填入数字1~9,每个数字在每个单元中只能出现一次解答数独。人脑求解数独是最为普遍的方法,思考数独求解的过程,可以活跃思维,给思维带来无穷的乐趣。人工求解数独的过程,需要用到很多技巧,有列排除法,行排除法,行列排除法,唯一性法,单元格对法等技巧。而这些技巧都是建立在遵守约束条件的基础上发明创造的。计算机语言求解数独的过程,是一个模拟人脑思考数独的过程,这个过程程序化高,准确率更高,思考时间更短。论文主要使用递归法和回溯法求解数独。

3.1 求解约束条件

无论是人工求解还是计算机求解,都遵循一个共同的规律,就是计算机在9×9的空格中填满1-9的数字,要求大正方形每一行、每一列及每个九宫格内均必须包括19的每一个数字,不重复也不遗漏。从数独的规则,得出解决数独游戏必须遵守的约束条件:

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计算空格数

View Code
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计算空格自由度

View Code
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则是表示数独的属性类,类中包含了函数执行的基本方法,如逐个读取空格和查找空白,判读所填数字是否符合各种条件。

View Code
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:
优质内容筛选与推荐>>
1、硬盘2T 现在也才不到1000了,变得感觉太便宜了
2、mongodb进阶三之mongodb管理
3、使用DTS导出到Access,FoxPro,Word ,Excel 不需要验证的大批量数据。
4、7.2.3、数组排序
5、CSS布局设计的基础:块元素(block element)和 内联元素(inline element)


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn