在LeetCode上偶然刷到一个解数独的题目:
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
一个数独。
答案被标成红色。
Note:
- 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
- 你可以假设给定的数独只有唯一解。
- 给定数独永远是 9x9 形式的。
这个题目很是有趣,解决了数独算不出来的难题。
首先想到的是暴力枚举了,也就是对每个空格进行枚举,回溯法。
public void solveSudoku(char[][] board) {
//boolean数组 表明是否被使用过,按照给定的规则
boolean[][] rows = new boolean[9][10];//行
boolean[][] cols = new boolean[9][10];//列
boolean[][] boxes = new boolean[9][10];//每个3*3小块
//先都初始化,有值的表示已被使用
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '0';
rows[i][num] = true;
cols[j][num] = true;
boxes[(i / 3) * 3 + j / 3][num] = true;
}
}
}
//开始回溯
bactTrack(board, rows, cols, boxes, 0, 0);
}
private boolean bactTrack(char[][] board, boolean[][] rows, boolean[][] cols, boolean[][] boxes, int i, int j) {
//当回溯到达每行的边界时,进入下一行
if (j == board[0].length) {
j = 0;
i++;
//如果都遍历完成,也就是到达(9,9)处,找到其中一个解
if (i == board.length) {
return true;
}
}
//如果是 空 格
if (board[i][j] == '.') {
for (int num = 1; num <= 9; num++) {
int boxIndex = (i / 3) * 3 + j / 3;//这个时将i,j下标映射到对应的3*3小块中
boolean isUesd = rows[i][num] || cols[j][num] || boxes[boxIndex][num];//判断是否被使用
//如果没有被使用
if (!isUesd) {
rows[i][num] = true;
cols[j][num] = true;
boxes[boxIndex][num] = true;
board[i][j] = (char) ('0' + num);
//进入下一步
if (bactTrack(board, rows, cols, boxes, i, j + 1)) {
return true;
}
//如果下一步无解,回溯
board[i][j] = '.';
rows[i][num] = false;
cols[j][num] = false;
boxes[boxIndex][num] = false;
}
}
} else {//不是空格直接跳过
return bactTrack(board, rows, cols, boxes, i, j + 1);
}
//前面都没找到解
return false;
}
以上的算法便是根据给出的数独得到数独的解。
然后突然想试试这个算法,但是没有生成数独的算法,找到了数独-- 一个高效率生成数独的算法博客里的方法,也验证了下,能达到随机生成的效果(虽然都是伪随机)
//seedArray是用来生成数独的种子数组
private void creatSudokuArray(int[][] seedArray) {
//产生一个1-9的不重复长度为9的一维数组
ArrayList<Integer> randomList = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 9; i++) {
int randomNum = random.nextInt(9) + 1;
while (true) {
if (!randomList.contains(randomNum)) {
randomList.add(randomNum);
break;
}
randomNum = random.nextInt(9) + 1;
}
}
/*
通过一维数组和原数组生成随机的数独矩阵
遍历二维数组里的数据,在一维数组找到当前值的位置,并把一维数组
当前位置加一处位置的值赋到当前二维数组中。目的就是将一维数组为
依据,按照随机产生的顺序,将这个9个数据进行循环交换,生成一个随
机的数独矩阵。
*/
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
for (int k = 0; k < 9; k++) {
if (seedArray[i][j] == randomList.get(k)) {
seedArray[i][j] = randomList.get((k + 1) % 9);
break;
}
}
//将生成的重新赋值给需要的,seedArray不做改变
board[i][j] = seedArray[i][j];
}
}
}
好了,有了生成数独的算法后,但是这只是个数独终盘,还需要挖空,最后利用随机数完成了不同等级的挖空
/**
* 给指定数独终盘挖空,利用随机数,随机次数为level,也就是对每行挖level次空
* 难度对应easy--5, middle--7, hard--9
* 每次挖空后判断是否有解,若无解重新挖空
* @param level 挖空等级
*/
public void resetSudoku(int level) {
int[][] copies = new int[board.length][board[0].length];
if (level < 5) {
level = 5;
}
creatSudokuArray(seedArray);
Random random = new Random();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < level; j++) {
int ran = random.nextInt(9);
board[i][ran] = 0;
}
}
if (!solveSudoku(copies)) {
resetSudoku(level);
}
}
好了,有了这一系列的算法后,便可以自己写一个数独游戏了。
图片示意如下:
Github:DiyViewPracticeDemo