[LeetCode 37] Sudoku Solver回溯解法

原题:

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;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值