数独是一种数学逻辑游戏,游戏由9×9个格子组成,玩家需要根据格子提供的数字推理出其他格子的数字。游戏设计者会提供最少17个数字使得解答谜题只有一个答案。
数独的解法需 遵循如下规则:
虽然玩法简单,但提供的数字却千变万化,所以很适合用程序来求解。
类似这种需要穷举的问题一般采用回溯法,也就是暴力求解,在最坏的情况下时间复杂度为指数。
回溯法
回溯法采用试错的思想,它尝试分步的去解决一个问题。在解决问题的过程中,如果现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
回溯法通常用最简单的递归方法来实现,下面是回溯法的一般结构。
理解回溯法首先要理解递归,理解递归的过程,以及递归的返回值。可以通过查看斐波那契数列的递归实现的调用栈来理解递归的过程。
1 | def backtracking(args): |
暴力解法
回到数独解法上来
首先看最简单的暴力解法
这里有几个点和模板不一样
- 由于把整个棋盘都填满游戏就终止了,所以不需要
can_stop
判断 - 每层递归会填满一个空位,递归的最大深度就是空位的个数。
- 数独棋盘都填满才算一个解,所以每层递归
get_all_choices
最多有行 x 列 x 数字取值 = 9 x 9 x 9
种情况 - 由于数独规则的约束,行、列、九宫格内数字不能重复,所以天然有部分剪枝
1 | # 0 代表空位 |
剪枝
所谓的剪枝,就是递归每一层的时候,并不是所有情况都是有效的,可以跳过这些分支。
以该题为例,第一行第三列可选取值并不是1到9,而是1,2,4
,而且随着我们不断把空位填满,越后面的点选择越少。
可以用集合存储每个空位的行、列、九宫方向的可选值,三者交集就是该点的可选值。(用位替换集合还可以进一步优化算法)
因为填充一个空位,会影响该位置行、列、九宫上的所有空位的取值,所有不直接存储该点的可选值。
1 | EMPTY = 0 |
参考
https://zh.wikipedia.org/wiki/%E6%95%B8%E7%8D%A8
https://zh.wikipedia.org/wiki/%E5%9B%9E%E6%BA%AF%E6%B3%95
https://www.jianshu.com/p/8e694d079a76