回溯法之数独 详解

我从高中起就很喜欢数独这种娱乐方式,喜欢那种为了解出一个数独的执着,喜欢那份坚持,出于这种心情,我选择用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次生成完整数独的过程,程序并没有出现错误,但是隐患肯定是存在的,希望各位大大指教.

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值