原题:
Write a program to solve a Sudoku puzzle by filling the empty cells.
Empty cells are indicated by the character '.'.
You may assume that there will be only one unique solution.
解决一个数独,我认为还是比较直观的,一直觉得计算机或者算法就是人类思维的一个载体而已,只不过它不会累,可以工作、记忆更久。所以我们直接按照自己解题的想法来就可以了。
我选择的方法还是按照人类思维来的,如果追求代码简单的话,可以跳过找最小候选集合、找唯一解的过程,直接完全暴力回溯,尝试在空位填入所有可能的解,毕竟最坏时间复杂度是一样的,不失为一种解题的思路。
Step 1
在我们解决数独的过程中,首先一定是找有唯一解的位置,然后逐步把所有拥有唯一解的位置填满,直到所有空格都没有唯一解。
这一步,我们可以给出一个set<int> s[9][9]
,其中存放了所有点的可能候选解(注意,如果该点已经有数字了,那么这个集合没有意义,但是开一个二维数组可以帮助我们快速索引)。
我们首先把所有点的候选解设置为1~9的所有数字,然后遍历整个数独,在集合中不断erase掉所有同一行、同一列、同一个box出现过的数字。这一步是O(n^3)。
然后我们从中找到那些拥有唯一解的集合,放进一个队列里面,我们用一个数据结构同时保存这个点的row、col和set,并且队列里存放的是该数据结构的指针。
Step 2
对于这个队列里的每一个元素,要么无解,要么有唯一解,对于队列首部的元素q.front()
,我们这样考察:
1. 如果这个元素的候选解集合为空,那么该数独无解,直接返回,这一步是为了回溯。(这里可能会很奇怪,明明放进来的是有唯一解的元素,为什么会空呢,往下看)
2. 如果这个元素的候选解唯一,那么我们首先把board的相应位置修改为这个解,另外,我们要让所有与这个元素同一行、同一列或者同一个box的位置的候选集合都去掉这个数字,这可以依靠我们的二维数组set快速做到,最后,我们把那些erase掉数字后候选集合大小为1的集合放进队列里。
这一步最坏情况下是O(n^2),而且在信息量比较大的数独求解的过程中可以加速解题的速度。
Step 3
我们最终会发现队列空了,这可能有两种情况:要么解题成功,要么所有空格都没有唯一解。
很简单的是,我们遍历一遍数独就可以知道是否解题成功,如果没有,我们要找出候选集合最小的那个位置,然后我们在这个位置分别填上所有候选解。
比如(0, 0)的位置可以填1,2,那么我们先填上1,这样一来我们得到了一个更加有可能可以求解的数独,OK,对这个新的board,递归求解就可以了。
下面给出AC代码:
struct node
{
int row, col;
set<int> s{0,1,2,3,4,5,6,7,8};
};
// 放入队列的数据结构
class Solution {
public:
int solved;
vector<vector<char>> solvedBoard;
void solve(vector<vector<char>> board, queue<node*> couldBeSolved, node v[][9]){
while(!couldBeSolved.empty()){
auto node = *(couldBeSolved.front());
if (node.s.size() == 0)
return;
// 如果没有唯一解,那么该board无解,回溯
int row = node.row, col = node.col;
int sol = *(node.s.begin());
board[row][col] = sol + '1';
for (int i = 0; i < 9; ++i)
{
// 这个循环是当我们填上唯一解的时候,要对整行整列整个子box去掉解的值
if (v[row][i].s.find(sol)!=v[row][i].s.end())
{
v[row][i].s.erase(sol);
if (board[row][i] == '.' && v[row][i].s.size() == 1){
couldBeSolved.push(v[row]+i);
}
}
if (v[i][col].s.find(sol) != v[i][col].s.end())
{
v[i][col].s.erase(sol);
if (board[i][col] == '.' && v[i][col].s.size() == 1){
couldBeSolved.push(v[i]+col);
}
}
int boxIndex = 3*(row/3)+col/3;
int boxRow = 3*(boxIndex/3)+i/3, boxCol = 3*(boxIndex%3)+i%3;
if (boxRow != row && boxCol != col && v[boxRow][boxCol].s.find(sol) != v[boxRow][boxCol].s.end())
{
v[boxRow][boxCol].s.erase(sol);
if (board[boxRow][boxCol] == '.' && v[boxRow][boxCol].s.size() == 1){
couldBeSolved.push(v[boxRow]+boxCol);
}
}
}
couldBeSolved.pop();
}
int minsize = 10, minrow, mincol;
for (int i = 0; i < 9; ++i)
for (int j = 0; j < 9; ++j)
if (board[i][j] == '.' && v[i][j].s.size() < minsize)
{
minrow = i;
mincol = j;
minsize = v[i][j].s.size();
}
// 找到候选集合最小的那个点
if (minsize == 0)
{
// 无解,返回
return;
}
else if(minsize == 10){
// 说明没有空格存在了,直接返回
solved = true;
solvedBoard = board;
}
else{
for (auto iter = v[minrow][mincol].s.begin(); iter != v[minrow][mincol].s.end(); iter++)
{
vector<vector<char>> newBoard = board;
newBoard[minrow][mincol] = *iter + '1';
if(solved == false)solveSudoku(newBoard);
else break;
}
// 复制一份board,然后填上一个数字,递归求解
}
}
void solveSudoku(vector<vector<char>>& board) {
node v[9][9];
solved = false;
for (int i = 0; i < 9; ++i)
{
for (int j = 0; j < 9; ++j)
{
v[i][j].row = i;
v[i][j].col = j;
if (board[i][j] != '.')
{
int num = board[i][j] - '1';
for (int k = 0; k < 9; ++k)
{
v[i][k].s.erase(num);
v[k][j].s.erase(num);
int boxIndex = 3*(i/3)+j/3;
v[3*(boxIndex/3)+k/3][3*(boxIndex%3)+k%3].s.erase(num);
}
}
}
}
// 求解所有的node,node里保存了每个点的位置和候选解
queue<node*> couldBeSolved;
for (int i = 0; i < 9; ++i)
for (int j = 0; j < 9; ++j)
if(board[i][j] == '.' && v[i][j].s.size() == 1)couldBeSolved.push(v[i]+j);
else if (board[i][j] == '.' && v[i][j].s.size() == 0)return;
// 让候选解size为1的点入队列
solve(board, couldBeSolved, v);
// 求解
board = solvedBoard;
}
};