一.题目描述
NC47数独
题目链接:
https://www.nowcoder.com/practice/5e6c424b82224b85b64f28fd85761280?tpId=196&&tqId=37077&rp=1&ru=/activity/oj&qru=/ta/job-code-total/question-ranking
请编写一个程序,给数独中的剩余的空格填写上数字,空格用字符'.'表示,假设给定的数独只有唯一的解法。
二.算法(搜索)
图片说明
思考:利用暴力搜索对每一种情况进行合法性判断,可以设置三个搜索变量dfs(board,x,y):
(1)(x,y)表示搜索坐标,那么每一个小数组中任意一个坐标可以表示为:
1.横坐标-(x/3)3+i/3
2.纵坐标-(y/3)
3+i%3
(2)board表示要被搜索的数独二维数组
思路:经典的搜索问题,和八皇后问题类似,关键实现一个合法性判断函数,isValid(x,y,c)即在(x,y)位置放入c字符是否可行 ,即行不能重复 ,列不能重复,九宫格不能重复dfs(x,y,c) 当遇到存在字符时跳过。空格时,填入isValid可行的字符,并跳转到下一个dfs(board,x,y+1),当index==81即所有字符填充完毕时,是最终结束搜索,迅速返回,可采取标记变量方式。
下面是代码:

#include <vector>

using namespace std;

class Solution {
public:
    void solveSudoku(vector<vector<char> > &board) {
        //从(0,0)位置开始搜索
        dfs(board, 0, 0);
    }
    bool dfs(vector<vector<char> > &board, int x, int y) {
        if (x==9) return true;
        if (y==9) return dfs(board,x+1,0);//表示该列已经检验完了 对下一行进行检验
        //如果被数字占位置了 直接进入下一列
        if (board[x][y] != '.')  return dfs(board, x, y+1);
        //遍历1-9进行暴力判断 填入是否合法
        for (char c='1';c<='9';c++) {
            //利用isValid对填入字符进行判断是否合法
            if (!isValid(board, x, y, c)) continue;
            //回溯操作
            board[x][y] = c;
            if (dfs(board,x,y+1)) return true;
            board[x][y] = '.';
        }
        return false;
    }

    bool isValid(vector<vector<char> > &board, int x, int y, char ch) {
        for (int i = 0; i < 9; ++i) {
            //分别从三个方面进行检验
            //所在列是不是有相同数字
            if (board[x][i] == ch) return false;
            //所在行是不是有相同数字
            if (board[i][y] == ch) return false;
            //所在的小矩形是不是有相同数字
            if (board[(x/3)*3 + i/3][(y/3)*3 + i%3] == ch) return false;
        }
        return true;
    }
};

时间复杂度:o(n*n)其中n为数独的横纵大小
空间复杂度:o(1) 不需要开辟额外空间
优缺点:思考过程和实现简单,但是时间复杂度比较高

三.算法(位运算优化)
在算法二中,我们使用了暴力检验每一个状态是不是合法。我们同样也可以借助位运算,仅使用一个整数表示每个数字是否出现过。
具体地,数b的二进制表示的第i位(从低到高,最低位为第0位)为 11,当且仅当数字i+1已经出现过。例如当b的二进制表示为 (011000100)2时候,就表示3,7,8已经出现过。
图片说明
下面直接来看代码:

class Solution {
private:
    int line[9];
    int column[9];
    int block[3][3];
    bool valid;
    vector<pair<int, int>> spaces;//存储要填入的位置
public:
    void flip(int i, int j, int digit) {
        line[i]^=(1<<digit);//利用二进制压缩来表示行
        column[j]^=(1<<digit);//利用二进制压缩来表示列
        block[i/3][j/3]^= (1<<digit);//利用二进制要是来表示小数组
    }

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

        auto [i, j] = spaces[pos];
        //0x1ff 表示全为1 表全部填入了数字
        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
        for (; mask && !valid; mask &= (mask - 1)) {
            int digitMask = mask & (-mask);
            //找到最低位的非0位置
            int digit = __builtin_ctz(digitMask);
            //内置函数__builtin_ctz:返回前导的0的个数。
            //回溯
            flip(i, j, digit);
            board[i][j] = digit + '0' + 1;
            dfs(board, pos + 1);
            flip(i, j, digit);
        }
    }

    void solveSudoku(vector<vector<char>>& board) {
        memset(line, 0, sizeof(line));
        memset(column, 0, sizeof(column));
        memset(block, 0, sizeof(block));
        valid = false;
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    //要是没有填入直接放入spaces 进行填入
                    spaces.emplace_back(i, j);
                    //emplace_back省去了移动构造函数 效率高
                } else {
                    int digit = board[i][j] - '0' - 1;
                    flip(i,j,digit);
                }
            }
        }
        dfs(board, 0);
    }
};

时间复杂度:o(1)
空间复杂度:o(n * n)需要开辟额外的空间
优缺点:位运算优化难实现,但是时间效率很高