回溯算法一般就是解决八皇后问题和数独问题了,leetcode第37题 就是一道解数独题。输入是一个9x9的棋盘,空白格子用点号字符‘.’表示,算法需要在原地修改棋盘,将空白格子填上数字,得到一个可行解。
计算机解题思路其实和我们人类还是有区别的,人类解数独往往是先找容易确定的行、列和3×3的小方格,接着由此处往其他地方延伸;而计算机因为计算能力快可以采用暴力解法,直接遍历判断每行,每列以及每一个3×3的小方格都不能有相同的数字(1-9)出现。
算法函数签名如下:
class Solution {
public void solveSudoku(char[][] board) {
}
}
我们求解数独的思路简单粗暴,就是对每一个格子所有可能的数字进行穷举。对于每个位置,应该如何穷举,有几个选择呢? 从1到9就是选择,全部试一遍就行了:
// 对 board[i][j] 进行穷举尝试
private boolean backtrack(char[][] board, int i, int j) {
for (char ch = '1'; ch <= '9'; ch++) {
// 做选择
board[i][j] = ch;
// 继续穷举下一个
backtrack(board, i, j + 1);
// 撤销选择
board[i][j] = '.';
}
}
继续细化,现在只是给 j + 1,那如果 j 加到最后一列了,怎么办?并不是1到9都可以取到的,有的数字不是不满足数独的合法条件吗? 当 j 到达超过每一行的最后一列时,转为增加 i 同时把 j 设为0开始穷举下一行; 穷举之前添加一个判断,跳过不满足条件的数字:
private boolean backtrack(char[][] board, int i, int j) {
int m = 9, n = 9;
if (j == n) {
// 穷举到最后一列的话就换到下一行重新开始。
return backtrack(board, i + 1, 0);
}
// 如果该位置是预设的数字,不用我们处理,直接处理下一个格子
if (board[i][j] != '.') {
return backtrack(board, i, j + 1);
}
for (char ch = '1'; ch <= '9'; ch++) {
// 如果遇到不满足数独合法的数字,就跳过
if (!isValid(board, i, j, ch))
continue;
// 做选择
board[i][j] = ch;
// 继续穷举下一个
backtrack(board, i, j + 1);
// 撤销选择
board[i][j] = '.';
}
}
// 判断 board[row][col] 是否可以填入 n
private boolean isValid(char[][] board, int row, int col, char n) {
for (int i = 0; i < 9; ++i) {
// 判断行是否存在重复
if (board[row][i] == n) return false;
// 判断列是否存在重复
if (board[i][col] == n) return false;
// 判断 3 x 3 方格是否存在重复
if (board[(row/3)*3 + i/3][(col/3)*3 + i%3] == n) return false;
}
return true;
}
还有一个问题:这个算法永远不会停止递归。什么时候结束递归? 显然 i == m 的时候就说明穷举完了最后一行,完成了所有的穷举。
private boolean backtrack(char[][] board, int i, int j) {
int m = 9, n = 9;
if (j == n) {
// 穷举到最后一列就换到下一行重新开始。
return backtrack(board, i + 1, 0);
}
if (i == m) {
// 最后一行已经遍历完成,找到了一个可行解
return true;
}
// 如果该位置是预设的数字,不用我们处理,直接处理下一个格子
if (board[i][j] != '.') {
return backtrack(board, i, j + 1);
}
for (char ch = '1'; ch <= '9'; ch++) {
// 如果遇到不满足数独合法的数字,就跳过
if (!isValid(board, i, j, ch))
continue;
// 做选择
board[i][j] = ch;
// 进行下一步试探,发现当前选择能成功进行下去,就继续往下
if (backtrack(board, i, j + 1)) {
return true;
}
// 撤销选择
board[i][j] = '.';
}
// 这个位置把1-9都试过了,都无法继续下去,说明上一次的选择失败,需要回溯
return false;
}
// 判断 board[row][col] 是否可以填入 n
private boolean isValid(char[][] board, int row, int col, char n) {
for (int i = 0; i < 9; ++i) {
// 判断行是否存在重复
if (board[row][i] == n) return false;
// 判断列是否存在重复
if (board[i][col] == n) return false;
// 判断 3 x 3 方格是否存在重复
if (board[(row/3)*3 + i/3][(col/3)*3 + i%3] == n) return false;
}
return true;
}
最后我们再把整个9×9的方格加上去遍历填写,最终代码如下:
class Solution {
public void solveSudoku(char[][] board) {
// 非法数独
if (board == null || board.length != 9 || board[0] == null || board[0].length != 9)
return;
backtrack(board, 0, 0);
}
private boolean backtrack(char[][] board, int row, int col) {
if (col == 9) {
// 穷举到最后一列就换到下一行重新开始。
return backtrack(board, row + 1, 0);
}
if (row == 9) {
// 最后一行已经遍历完成,找到了一个可行解
return true;
}
for (int i = row; i < 9; ++i) {
for (int j = col; j < 9; ++j) {
// 如果该位置是预设的数字,不用我们处理,直接处理下一个格子
if (board[row][col] != '.') {
return backtrack(board, row, col + 1);
}
for (char ch = '1'; ch <= '9'; ch++) {
// 如果遇到不满足数独合法的数字,就跳过
if (!isValid(board, row, col, ch))
continue;
// 做选择
board[row][col] = ch;
// 进行下一步试探,发现当前选择能成功进行下去,就继续往下
if (backtrack(board, row, col + 1)) {
return true;
}
// 撤销选择
board[row][col] = '.';
}
// 这个位置把1-9都试过了,都无法继续下去,说明上一次的选择失败,需要回溯
return false;
}
}
return false;
}
// 判断 board[i][j] 是否可以填入 n
private boolean isValid(char[][] board, int row, int col, char n) {
for (int i = 0; i < 9; ++i) {
// 判断行是否存在重复
if (board[row][i] == n) return false;
// 判断列是否存在重复
if (board[i][col] == n) return false;
// 判断 3 x 3 方格是否存在重复
if (board[(row/3)*3 + i/3][(col/3)*3 + i%3] == n) return false;
}
return true;
}
}