回溯算法,解数独

397 阅读5分钟

回溯算法一般就是解决八皇后问题和数独问题了,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;
    }
}