今天是元宵佳节,又恰逢周末,原本应该出门闹花灯,但是当前的新冠疫情让大家关门闭户异常冷清。
猜灯谜是元宵节的传统项目,记得小时候在老家每个元宵节都打着灯笼出门,大街上满是彩灯和灯谜,城市还专门组织有大型的烟火表演,人山人海热闹非凡。
既然没法出门,在家也能做些个游戏,例如用看怎么用代码来解决数独问题吧:)
我们有如下数独问题:
这是一个9X9的数独矩阵,其中需要保证:
- 整数1~9在每一行中只能出现一次
- 整数1~9在每一列中只能出现一次
- 在其中9个3X3的子矩阵中,整数1~9也只能出现一次
通过以上限制,我们期望得到以下的答案:
为了能用程序来解决数独问题,我们用一个二维字符数组来代表字符矩阵:
char[][] board = {
{'5', '3', '.', '.', '7', '.', '.', '.', '.'},
{'6', '.', '.', '1', '9', '5', '.', '.', '.'},
{'.', '9', '8', '.', '.', '.', '.', '6', '.'},
{'8', '.', '.', '.', '6', '.', '.', '.', '3'},
{'4', '.', '.', '8', '.', '3', '.', '.', '1'},
{'7', '.', '.', '.', '2', '.', '.', '.', '6'},
{'.', '6', '.', '.', '.', '.', '2', '8', '.'},
{'.', '.', '.', '4', '1', '9', '.', '.', '5'},
{'.', '.', '.', '.', '8', '.', '.', '7', '9'}
};
其中未知的单元格用字符'.'来表示,期望实现个函数将所有'.'替换为'1'~'9'的字符并保证数独矩阵的合法性。
我们可以将此问题分解为两个子问题来考虑:检验放入一个值后,是否满足数独矩阵合法性;递归遍历整个矩阵,并在未知位置依次试探放入'1'~'9'并检查合法性,如果不合法则使用下一个值进行回溯分析。
校验当前单元格放入数值后的合法性
要校验合法性,满足数据矩阵所要求的三个规则即可:
/**
* 校验放入当试探值是否能保证数独矩阵的正确性
*
* @param board 数独矩阵
* @param x x轴坐标代表列号
* @param y y轴坐标代表行号
* @param c 试探放入的值,为'1'~'9'的整数
* @return 返回true当校验通过
*/
private boolean isValid(char[][] board, int x, int y, char c) {
//3*3方块的开始y轴号
int regionRow = 3 * (y / 3);
//3*3方块的开始x轴坐标
int regionCol = 3 * (x / 3);
for (int i = 0; i < 9; i++) {
//检查每一行的正确性
if (board[i][x] == c) return false;
//检查每一列的正确性
if (board[y][i] == c) return false;
//检查3*3方格内的正确性
if (board[regionRow + i / 3][regionCol + i % 3] == c) return false;
}
return true;
}
递归回溯分析整个矩阵
我们循环遍历整个二维数组,当遇到'.'后试探放入'1' ~ '9‘的整数并使用上面的方法进行正确性分析,如果正确后,递归调用下一个单元格,如果当前单元格放入'1' ~ '9'都不能得到合法的数独矩阵,则说明之前单元格放入了错误的值,此时回溯到上一次递归,试探放入下一个合法的整数,继续进行递归分析:
/**
* 递归函数,当得到合法的数据矩阵后返回true
*
* @param board 数独矩阵
* @param x x轴坐标代表列号
* @param y y轴坐标代表行号
* @return
*/
private boolean check(char[][] board, int x, int y) {
for (; y < 9; y++) {
//如果输入的列号溢出,则直接进入下一行从第一列开始
for (; x < 9; x++) {
if (board[y][x] != '.') continue;
//当前值为'.'时,
//依次试探放入'1'~'9'的值来看是否满足合法的数独矩阵
for (char c = '1'; c <= '9'; c++) {
//放入后不合法,直接忽略,尝试下一个值
if (!isValid(board, x, y, c)) continue;
board[y][x] = c;
//当前位置试探放入值c后,递归调用下一列
if (check(board, x + 1, y)) {
//当前值赋值c后,递归调用全部满足数独矩阵合法性,
//则返回成功
return true;
}
//当前位置放入c后,后续位置不能得到合法的数独矩阵,
//继续进行回溯分析,因为上次层递归调用要重写开始分析,
//所以要恢复当前的原始值
board[y][x] = '.';
}
//'1'~'9'全部试探完成后,
//依然无法获得合法数独矩阵,
//则说明之前放入的值有误,
//返回后上次层递归方法继续取其他值进行递归
return false;
}
//下一行,从第一列开始
x = 0;
}
//此时数组已经越界,
//说明全部递归完成没有发现非法数独矩阵,返回成功
return true;
}
验证
最后完成测试用例以及调用方法,进行结果验证:
public void solveSudoku(char[][] board) {
check(board, 0, 0);
}
public static void main(String[] args) {
Solution solution = new Solution();
char[][] board = {
{'5', '3', '.', '.', '7', '.', '.', '.', '.'},
{'6', '.', '.', '1', '9', '5', '.', '.', '.'},
{'.', '9', '8', '.', '.', '.', '.', '6', '.'},
{'8', '.', '.', '.', '6', '.', '.', '.', '3'},
{'4', '.', '.', '8', '.', '3', '.', '.', '1'},
{'7', '.', '.', '.', '2', '.', '.', '.', '6'},
{'.', '6', '.', '.', '.', '.', '2', '8', '.'},
{'.', '.', '.', '4', '1', '9', '.', '.', '5'},
{'.', '.', '.', '.', '8', '.', '.', '7', '9'}
};
solution.solveSudoku(board);
System.out.println("{");
for (char[] row : board) {
System.out.print(" {");
String split = "";
for (char c : row) {
System.out.print(split + c);
split = ",";
}
System.out.println("},");
}
System.out.println("}");
}
得到正确结果:
{
{5,3,4,6,7,8,9,1,2},
{6,7,2,1,9,5,3,4,8},
{1,9,8,3,4,2,5,6,7},
{8,5,9,7,6,1,4,2,3},
{4,2,6,8,5,3,7,9,1},
{7,1,3,9,2,4,8,5,6},
{9,6,1,5,3,7,2,8,4},
{2,8,7,4,1,9,6,3,5},
{3,4,5,2,8,6,1,7,9},
}
好了,以后这种数独问题我们都能在几毫秒内解决它了:)