解数独

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

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

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。

1. 判断当前数独是否有效

遍历完整个数独空间需要n*n次运算,若是再对每个位置的数判断其竖向、横向、九宫格内是否符合要求
所需的运算时间更是庞大,我们可以发现再次判断的很多过程都属于重复运算,最优的时间复杂度应该是O(1),即遍历一次
维护横向、纵向、九宫格内的哈希表,并使它们随着遍历更新,可以避免重复判断,以空间换时间

判断有效性(优化)
class Solution {
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        //使用bool而不是int内存消耗更小
        bool row[9] = {0};// 哈希表存储每一行的每个数是否出现过,默认初始情况下,每一行每一个数都没有出现过,随着遍历会使索引表逐渐变大,由于后面是一行一行遍历,可以使用一维空间减少内存消耗
        bool col[9][9] = {0};// 存储每一列的每个数是否出现过,默认初始情况下,每一列的每一个数都没有出现过
        bool box[3][9] = {0};// 存储每一个九宫格的每个数是否出现过,默认初始情况下,在每个box中,每个数都没有出现过,整个board有9个box,由于行遍历,只需暂存三个
        for(int i=0; i<9; i++){
            for(int j = 0; j<9; j++){
                if(board[i][j] == '.') continue;
                int curNumber = board[i][j]-'1';//数组从0开始,所以减一把值变成索引
                //判断这个数在其所在的行有没有出现过
                if(row[curNumber]) return false; 
                 // 同时判断这个数在其所在的列有没有出现过
                if(col[j][curNumber]) return false;
                // 同时判断这个数在其所在的box中有没有出现过
                if(box[j/3][curNumber]) return false;

                row[curNumber] = 1;// 之前都没出现过,现在出现了,就给它置为1,下次再遇见就能够直接返回false了
                col[j][curNumber] = 1;
                box[j/3][curNumber] = 1;
            }
        //行索引重新初始化,节省空间
        for(int i=0;i<9;i++){row[i]=0;}
        //九宫格索引重新初始化
        if (i == 2 || i == 5) {
            for (int k = 0; k < 3; k++) {
                for(int i=0;i<9;i++){box[k][i]=0;}
                }
            }
        }
        return true;
    }
};

官方写法(未优化)
class Solution {
public:
    bool isValidSudoku(vector<vector<char>>& board) {
        int rows[9][9];
        int columns[9][9];
        int subboxes[3][3][9];
        
        memset(rows,0,sizeof(rows));
        memset(columns,0,sizeof(columns));
        memset(subboxes,0,sizeof(subboxes));
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c != '.') {
                    int index = c - '0' - 1;
                    rows[i][index]++;
                    columns[j][index]++;
                    subboxes[i / 3][j / 3][index]++;
                    if (rows[i][index] > 1 || columns[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
};

2. 回溯法解数独问题

类似求解N皇后问题,遍历每一行,每一列,同时递归选择1~9九个数
实际上,不需要每一次递归的时候,都重复遍历每一行每一列
我们需要遍历的实质上只是未插入的位置,而且随着值的插入,这个遍历空间还会逐渐变小
所以事先可以通过一次遍历,初始化横向、纵向、九宫格哈希表和待插入空间,接着再进行递归

class Solution {
private:
    //初始化哈希表为私有全局变量,方便递归使用
    bool line[9][9]={0};
    bool column[9][9]={0};
    bool block[9][9]={0};
    bool valid = 0;
    vector<pair<int, int>> spaces;//对数数组用于记录第n个待插入位置

public:
    void solveSudoku(vector<vector<char>>& board) {
        //初始化索引表和待选择位置
        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.emplace_back(i, j);
                }
                else {
                    int digit = board[i][j] - '1' ;
                    line[i][digit] = column[j][digit] = block[i/3+(j/3)*3][digit] = true;
                }
            }
        }
        //递归从第一个待选择位置开始
        backtrack(board, 0);
    }

    void backtrack(vector<vector<char>>& board, int pos) {
        if (pos == spaces.size()) {//遍历完所有未插入位置跳出
            valid = true;
            return;
        }
        auto [i, j] = spaces[pos];//获取当前未插入的位置
        for (int digit = 0; digit < 9 && !valid; ++digit) {//遍历1~9九个数
            if (!line[i][digit] && !column[j][digit] && !block[i/3+(j/3)*3][digit]) {//不冲突
                line[i][digit] = column[j][digit] = block[i/3+(j/3)*3][digit] = true;//选择
                board[i][j] = digit + '1' //该位置分别做选择
                backtrack(board, pos + 1);//递归到下一个位置
                //出递归,相当于撤销该位置选择,重新选择其他值
                line[i][digit] = column[j][digit] = block[i/3+(j/3)*3][digit] = false;//撤销选择,为该位置做其它选择提供条件
            }
        }
    }
};

回溯法思考
回溯递归的关键在于对该位置所有选择进行递归
递归的时候一定要进行一个选择,进入到下一个状态
递归结束后撤销上一次的选择


以本题算法为例,回溯递归的框架为
backtrack(board,pos){//每个位置选了数之后进入下一状态,即每个位置遍历九个数,进入九个子状态
  for(遍历1~9个数){
  修改board
  backtrack(board,pos+1)//进入到下一个递归的时候,board和pos都已经发生改变,即作出了选择,转移到了下一个状态
  //pos在传入参数进行的修改,递归结束的时候自动会撤回,但board是全局变量,不会因为递归结束撤回选择
  改回board //所以还要改回board和相关的哈希表,表示一次完整的撤销选择
  }
}
//由于board是所有递归共用的,所以我们必须按位置有序递归,否则会发生冲突


3. 位运算进阶优化

用一个整数的二进制表示一个布尔数组,进一步减少内存空间的使用,同时事前将可以唯一确定的数填入,减少递归

位运算
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];
        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
        for (; mask && !valid; mask &= (mask - 1)) {
            int digitMask = mask & (-mask);
            int digit = __builtin_ctz(digitMask);
            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] != '.') {
                    int digit = board[i][j] - '0' - 1;
                    flip(i, j, digit);
                }
            }
        }

        while (true) {
            int modified = false;
            for (int i = 0; i < 9; ++i) {
                for (int j = 0; j < 9; ++j) {
                    if (board[i][j] == '.') {
                        int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
                        if (!(mask & (mask - 1))) {
                            int digit = __builtin_ctz(mask);
                            flip(i, j, digit);
                            board[i][j] = digit + '0' + 1;
                            modified = true;
                        }
                    }
                }
            }
            if (!modified) {
                break;
            }
        }

        for (int i = 0; i < 9; ++i) {
            for (int j = 0; j < 9; ++j) {
                if (board[i][j] == '.') {
                    spaces.emplace_back(i, j);
                }
            }
        }

        dfs(board, 0);
    }
};

posted @ 2022-05-13 13:31  失控D大白兔  阅读(35)  评论(0编辑  收藏  举报