我从高中起就很喜欢数独这种娱乐方式,喜欢那种为了解出一个数独的执着,喜欢那份坚持,出于这种心情,我选择用java来实现一个数独从无到有的过程。喜爱玩数独的人应该知道,数独的限制条件很有意思:首先,一个9*9的九宫格被分成了九个区域,如图所示,不同颜色区域划分成不同区域。
限制条件1:同一个格子内(色块)不能出现相同的数;
限制条件2:同一行内不能出现相同的数字
限制条件3:同一列内不能出现相同的数字
如何在满足上述条件的同时,还将这81个格子填满?这就是我今天要分享给大家的内容。
首先,我们可以将问题简化一下,我可以先把数字一存放到格子中,然后放置数字二,知道存放数字九,将数字一(二、三、...、九)放入九宫格的过程和八皇后问题非常相似(关于八皇后问题可以查看我上一篇博客八皇后问题),所以现在问题很简单了,问题就变成了依次将数字1~9放入九宫格,但是,问题不会就这么简单的,在实现代码的过程中,我发现,如果在放置数字7的时候,没有合法的位置(这种情况显然是存在的),那么现在就需要回溯到上一个数字6了,但是问题出现了,在针对每个数字的回溯的时候,我明确的知道如果一行都无法方式放置,就该回溯了,放第一行没有合法位置放置,我就该回溯到上一个数字6,但是,回溯到数字6时,我怎么确定回溯到数字5的条件呢?关于这个数值是不确定的,数字6到数字5的阀值和数字5到数字4的阀值肯定是不同的?想了很久还是没能想象出完美的解决方法,我决定退而求其次,我将阀值设定为100,也就是说,到了100的阀值,我就继续向上回溯。
程序代码如下:
/**
* 生成一个合法的数独 该算法存在一个未解决的问题: 现在,每次回溯到上一个数字时,我选择了将上一个数的位置全部重置,再进行下一个数的回溯
* 这样就无法确定回溯到再上一位数字的阀值,最后我选择了自行定义一个阀值100,即如果100次回溯后还未解决我就继续向上回溯
* 这种自己设立的阀值肯定是存在隐患的,但是我实在想不到如何确定准确的阀值,只能出此下策了
*/
public void getAnswer() {
int times = 0;
for (int i = 1; i <= SIZE; i++) {
if (numPosition(i) == -1) {
// 下一个数字无法放置 回溯到上一个数字
times++;
if (times < 100) {
i--;
System.out.println("回溯当上一个数字" + i);
clear(i);
numPosition(i);
} else { // 如果经过100次重置上一行还不能得到结果,则再向上回溯一层 100是自己设置的值
i--;
clear(i);
clear(i - 1);
numPosition(i - 1);
i--;
times = 0;
}
}
}
}
如果有大佬有解决的思路,求分享!!!
整个程序的代码如下:
public class Sudoku {
public static final int SIZE = 9; // 数独大小
public int[][] Num = new int[SIZE][SIZE];
public int[][] SpeedDial = new int[SIZE][SIZE];
public Sudoku() {
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
SpeedDial[i][j] = 0; // 九宫格初始化为0
Num[i][j] = -1; // 存放位置初始化为-1
}
}
}
/**
* 生成一个合法的数独 该算法存在一个未解决的问题: 现在,每次回溯到上一个数字时,我选择了将上一个数的位置全部重置,再进行下一个数的回溯
* 这样就无法确定回溯到再上一位数字的阀值,最后我选择了自行定义一个阀值100,即如果100次回溯后还未解决我就继续向上回溯
* 这种自己设立的阀值肯定是存在隐患的,但是我实在想不到如何确定准确的阀值,只能出此下策了
*/
public void getAnswer() {
int times = 0;
for (int i = 1; i <= SIZE; i++) {
if (numPosition(i) == -1) {
// 下一个数字无法放置 回溯到上一个数字
times++;
if (times < 100) {
i--;
System.out.println("回溯当上一个数字" + i);
clear(i);
numPosition(i);
} else { // 如果经过100次重置上一行还不能得到结果,则再向上回溯一层 100是自己设置的值
i--;
clear(i);
clear(i - 1);
numPosition(i - 1);
i--;
times = 0;
}
}
}
}
// 清除数字num
public void clear(int num) {
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if (SpeedDial[i][j] == num) {
SpeedDial[i][j] = 0;
}
}
}
}
// 确定数字num的位置数组
public int numPosition(int num) {
int j = 0;
for (int i = 0; i < SIZE; i++) {
j = findPosition(i, num);
if (j < 0) { // 死路
i--;
if (i < 0) { // 当第一行都无法放置时,回溯到上一个数字
resetFirst();
return -1;
}
if (i == 0) {
// 回溯到第一行
System.out.println("数字" + num + "回溯到第一行");
SpeedDial[i][Num[num - 1][i]] = -10;
reset();
} else {
// 回溯到上一行
System.out.println("数字" + num + "在" + i + "行向上回溯");
SpeedDial[i][Num[num - 1][i]] = -10;
}
i--; // 重新选定位置
} else {
Num[num - 1][i] = j; // 数字num存放在i行 j列 即Num[num-1][i]存放的数值是j
SpeedDial[i][j] = num;
}
}
// 回溯一次后 将此次过程标记的状态全部清空 防止影响下一次回溯过程
resetAll();
return j = 0;
}
// 清除第一行痕迹
public void resetFirst() {
for (int j = 0; j < SIZE; j++) {
if (SpeedDial[0][j] == -10) {
SpeedDial[0][j] = 0; // 重置回0
}
}
}
// 清除除第一行之外的标记
public void reset() {
for (int i = 1; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if (SpeedDial[i][j] == -10) {
SpeedDial[i][j] = 0; // 重置回0
}
}
}
}
// 清除所有标记
public void resetAll() {
resetFirst();
reset();
}
public int findPosition(int i, int num) {
int j = (int) (Math.random() * 8);
for (int k = 0; k < SIZE; k++) {
if (isViald(i, j, num)) {
return j;
}
j = (j + 1) % SIZE;
}
return -1;
}
// 判断位置是否合法
public boolean isViald(int row, int column, int num) { // 对应在数组中的位置(从0开始)
// 判断是否标记为死路
if (SpeedDial[row][column] == -10) {
return false;
}
// 判断当前位置是否防止其他数字
if (SpeedDial[row][column] != 0) {
return false;
}
// 判断在同一个格子里是否出现相同数字
for (int i = ((row) / 3) * 3; i < ((row) / 3) * 3 + 3; i++) {
for (int j = ((column) / 3) * 3; j < ((column) / 3) * 3 + 3; j++) {
if (SpeedDial[i][j] == num) {
return false;
}
}
}
// 判断同一列是否出现出现相同数 (无需考虑同一行,因为程序的执行就排除了同行出现相同数的情况)
for (int i = 0; i < row; i++) {
if (SpeedDial[i][column] == num) {
return false;
}
}
return true;
}
}
Test.java
public class Test {
public static void main(String[] args) {
for (int k = 0; k < 5000; k++) {
Sudoku sudoku = new Sudoku();
sudoku.getAnswer();
for (int i = 0; i < sudoku.SIZE; i++) {
for (int j = 0; j < sudoku.SIZE; j++) {
System.out.print(sudoku.SpeedDial[i][j] + " ");
}
System.out.println();
}
}
}
}
我测试了5000次生成完整数独的过程,程序并没有出现错误,但是隐患肯定是存在的,希望各位大大指教.