解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 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);
}
};