0
点赞
收藏
分享

微信扫一扫

力扣37. 解数独 (递归回溯或位运算)

目标践行者 2022-03-21 阅读 29

题目:编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则:

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

案例:

        输入: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"]]
        输出:[["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"]]

        解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

----------------------------------------------

解题:

这道题与力扣51题 N皇后 的解题思路非常像,可以用穷举递归算法来遍历每个没有填数字的位置,即 '.'。

首先要把9x9的数独格子遍历一遍,找出并用一个数组记录 '.' 所在的位置,这个位置是以后递归需要处理的位置。遍历的同时,我们还需要用三个数组来记录每行,每列,每个3x3的九宫格中,1-9是否出现过。遍历完毕后,我们从记录 '.' 所在的位置数组的位置0开始递归,每个位置穷举填入1-9的情况,如果将要填进去的数字已经在此行,或者此列,或者九宫格内出现过了,不继续递归,检查下一个数字,如果还没有出现过,则继续在数组的下一个位置进行递归。如果能到达数组的最后一位,递归完毕,则表明能到了一个答案。题目曾提示说,能找到唯一有效的答案,所以一定能找出一个答案。

解题思路很好理解,下文的 思路一 提供了通用的数据结构递归回溯法。本题的难点在于如何做空间优化, 51. N皇后 和本题的数据结构优化都用到了位运算,如何实现优化会在下文的 思路二 中提及。

思路一,递归回溯。

此思路用到了三个记录bool的数组rows[9][9],columns[9][9],ninebox[9][9]来记录数字n是否在某行某列某九宫格里出现过了。比如,数字 2(n) 在第i行 第j列 出现过,则记录rows[i-1][1],columns[j-1][1],ninebox[i/3*3+j/3][1]为True。这里记录九宫格的位置比较自由,可以用本文的ninebox[i/3*3+j/3][n-1],也可以用更直观的方式ninebox[i/3][j/3][n-1],/此符号为向下取整。

具体的步骤可以看C++的代码:

class Solution {
    
public:
    // 记录是否遍历到了最后一个位置,是,则不用回溯,否,则要回溯。这里对于回溯非常关键    
    bool valid;
    // 记录行列九宫格中是否出现过1-9的数字
    bool rows[9][9];
    bool columns[9][9];
    bool ninebox[9][9];
    // 记录9x9的数独格子中需要遍历的位置
    // 如果把所有位置进去递归,回溯的时候会改变解题前填入rows,columns,ninebox的检查状态
    vector<pair<int, int>> emptypos;
    
    // 递归公式
    void dfs(int pos, vector<vector<char>>& board){
        // 检查完最后一个'.'位置则返回,并不用回溯
        if(pos == emptypos.size()) {
            valid=true;
            return;
        }
        
        // 提取此'.'所在行列位置
        auto [i, j] = emptypos[pos];
        
        // 还从1-9开始逐个检查是否能填
        // 能填则递归下一个位置,不能填则返回。
        for(int n =0;n<9&&!valid;n++){
            // 能填,满足行列九宫格没出现过的条件
            if(!rows[i][n]&&!columns[j][n]&&!ninebox[i/3*3+j/3][n]){
                // 向9x9的数独格中填入数字
                board[i][j]='1'+n;
                // 修改状态
                rows[i][n]=1;
                columns[j][n]=1;
                ninebox[i/3*3+j/3][n]=1;
                // 进行下一个位置的递归
                dfs(pos+1, board); 
                // 不满足条件,回溯后,需要退回到原来的状态    
                rows[i][n]=0;
                columns[j][n]=0;
                ninebox[i/3*3+j/3][n]=0;                   
                }                  
            }     
    }

    // 解数独  
    void solveSudoku(vector<vector<char>>& board) {
        // 初始化状态
        memset(rows, false, sizeof(rows));
        memset(columns, false, sizeof(columns));
        memset(ninebox, false, sizeof(ninebox));
        valid = false;

        // 解数独前遍历每个格子,记录状态,查找空白格'.'位置 
        for(int i =0;i<9;i++){
            for(int j=0;j<9;j++){
                if(board[i][j]!='.'){
                    int n = board[i][j]-'1';
                    rows[i][n]=1;
                    columns[j][n]=1;
                    ninebox[i/3*3+j/3][n]=1;
                }else emptypos.emplace_back(i, j);
            }
        }
        
        // 从第一个位置递归起
        dfs(0,board);

        return;
    }
};

思路二,位运算。

位运算思路并没有改变思路一中递归回溯的框架,而是在数据结构和运算上有所优化。思路一中,记录行,列,九宫格,用到了二维的数组来记录 0 False,1 True。只有0和1时,就可以用二进制表示。位运算可以只用一个9位的int来记录状态,三个数组变成了记录int的一维数组rows[9],columns[9],ninebox[9]。比如1在某个位置出现过,那么此位置的int就是000 000 001,8,5,4出现过,就是010 011 000。如何检查某一为的1或0,如果改1为0,改0为1,需要熟练运用位运算。这里会列出需要记住的位运算,以便理解后面的代码。

1. | 或运算: 0001|0000 = 0001。找出两数出现过1的所有位置。

2. ∼ 按位取反运算:~0101 = 1010。0变成1,1变成0。连高位都会变成1。

3. & 与运算:1010 & 0110 = 0010。找出两数中均为1的位置。

        解题中有个地方做了 ∼ 按位取反运算,把高位变成了1,为了只算低位的则要与二进制的(111111111)2​ = (1FF)16 & 与。把高位变回来0。

4. ∧ 按位异或运算:0101 ∧ 1111 = 1010,1111 ∧ 0100 = 1011,1011 ∧ 0100 = 1111。可通过此方法反转某位上的1 和 0。比如将第三位的数字反转,可以将此数与(1<<3)做异或运算。

5. n&(-n):可得到n在二进制表示中最低位的1。位运算中的(-n)为∼n+1。-(0110) = 1001+1=1010,0110&1010=0010。

6. 内部函数 __builtin_ctz:计算出最低位1的位数。__builtin_ctz(0010)=2。

7. n&=(n-1):1110&(1110-1) = 1110&1101 = 1100。把最低位的1变成0。

记住了这些运算法则,就可以开始写代码实现解题了。具体步骤参照以下的C++代码:

class Solution {
    
public:
    bool valid;
    // 这里数组的类型和维度要改
    int rows[9];
    int columns[9];
    int ninebox[9];
    vector<pair<int, int>> emptypos;
    
    // 反转n位上的0和1,我们用到了与 1<<n 异或^
    void flip(int i, int j, int n){
        rows[i] ^= (1<<n);
        columns[j] ^= (1<<n);
        ninebox[i/3*3+j/3] ^= (1<<n);
    }

    void dfs(int pos, vector<vector<char>>& board){
        if(pos == emptypos.size()) {
            valid=true;
            return;
        }

        auto [i, j] = emptypos[pos];
        
        // 算出那些位置是能用的,就是用1记录可以使用的数,进行异或运算 
        // 反转所有0为1,还原高位上不必要的反转 &0x1ff  
        int mask = ~(rows[i]|columns[j]|ninebox[i/3*3+j/3])&0x1ff;

        // 循环逐一把低位的1变成0,mask&=(mask-1)
        for(;mask&&!valid;mask&=(mask-1)){
            // 只保留最低位的1,其余变成0 mask&(-mask)
            int digitMask = mask & (-mask);
            // 求最低位的1在那个位上
            int n = __builtin_ctz(digitMask);
            board[i][j]='1'+n;
            flip(i,j,n);
            dfs(pos+1, board); 
            flip(i,j,n);                       
        }     
    }

    void solveSudoku(vector<vector<char>>& board) {
        memset(rows, 0, sizeof(rows));
        memset(columns, 0, sizeof(columns));
        memset(ninebox, 0, sizeof(ninebox));
        valid=false;

        for(int i =0;i<9;i++){
            for(int j=0;j<9;j++){
                if(board[i][j]!='.'){
                    int n = board[i][j]-'1';
                    // 反转n位上的0
                    flip(i,j,n);
                }else{
                    emptypos.emplace_back(i,j);
                }
            }
        }

        dfs(0,board);

        return;
    }
};
举报

相关推荐

0 条评论