LeetCode 37. 解数独(难)
约束编程:基本的意思是在放置每个数字时都设置约束。在数独上放置一个数字后立即排除当前行、列和子方块对该数字的使用。这会传播 约束条件 并有利于减少需要考虑组合的个数。
回溯算法:让我们想象一下已经成功放置了几个数字在数独上。但是该组合不是最优的并且不能继续放置数字了。该怎么办? 回溯。意思是回退,来改变之前放置的数字并且继续尝试。如果还是不行,再次回溯。
题目
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。
注意:
- 给定的数独序列只包含数字 1-9
和字符 '.'
。 - 你可以假设给定的数独只有唯一解。 - 给定数独永远是 9x9
形式的。
示例
数独及其解,答案被标成红色。


考察知识点
哈希表、回溯算法
回溯算法模板
1
2
3
4
5
6
7
8
9
10result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径) # 路径就是要添加的结果
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
核心思想
方法一、暴力破解
首先的想法是通过蛮力法来生成所有可能用1 到 9填充空白格的解,
并且检查合法从而保留解。这意味着共有 \(9^{81}\)个操作需要进行。
其中 \(9\) 是可行的数字个数,\(81\) 是需要填充的格子数目。
因此我们必须考虑进一步优化。
方法二、回溯法
两个本方法中用到的重要概念
- 1、约束编程
基本的意思是在放置每个数字时都设置约束。在数独上放置一个数字后立即排除当前行、列和子方块对该数字的使用。这会传播 约束条件 并有利于减少需要考虑组合的个数。这个情况在N皇后问题中也出现过,N皇后问题也考虑了回溯算法。这说明约束编程和回溯算法一般都会复共同出现,约束编程可以视为一种剪枝策略。
- 2、回溯算法 让我们想象一下已经成功放置了几个数字在数独上。 但是该组合不是最优的并且不能继续放置数字了。该怎么办? 回溯。 意思是回退,来改变之前放置的数字并且继续尝试。如果还是不行,再次回溯。
获取子块id的方法 关于获取3x3方子块的方法,可以参照36题,方块索引= (行 / 3) * 3 + 列 / 3
,其中 /
是向下取整的除法。
算法 现在准备好写回溯函数了
backtrack(row = 0, col = 0)
- 从最左上角的方格开始 row = 0, col = 0。直到到达一个空方格,解决数独问题,也是一个3x3方格一个3x3方格的处理。 - 从 1
到 9
迭代循环数组,尝试放置数字 d 进入 (row, col) 的格子。 - 如果数字 d 还没有出现在当前行,列和子方块中: - 将 d 放入 (row, col) 格子中。 - 记录下 d 已经出现在当前行,列和子方块中。 - 如果这是最后一个格子row == 8, col == 8 : - 意味着已经找出了数独的解,退出。 - 否则 - 放置接下来的数字。 - 如果数独的解还没找到:将最后的数从 (row, col) 移除。
代码步骤(这道题的解法和N皇后很像)
1、定义could_place方法,检测该位置,是否可以放置该数字。
2、定义place_number方法,将数放置到对应位置,并添加rows、columns、boxes约束。
3、定义remove_number方法,将某个位置的数据去掉,替换回'.',同时去掉约束。
4、定义place_next_numbers方法。进行一些place_number之后的操作,比如终止循环、同行依次递归、异行折行递归。
4.1、如果是最后一行,最后一列,使用nonlocal关键字用来在函数或其他作用域中使用外层(非全局)。设置sudoku_solved为True 标记已经解决了该数独问题。
4.2、否则,判断是否是最后的一列,是的话折返去下一行。不然,就在本行中递归调用回溯函数。
5、定义backtrack回溯函数
5.1、如果当前位置是空的,也就是'.',就从1到10,一个一个的填进去试试。
5.1.1、先判断该位置能不能放这个数,如果没有被约束,就将这个数放到这个位置。
5.1.2、然后调用place_next_numbers,进行一些place_number之后的操作,比如终止循环、同行依次递归、异行折行递归。
5.1.3、如果没有解决,就执行撤销操作。
5.2、否则调用place_next_numbers方法,测下一个位置。
6、用lambad函数 计算box_index,初始化init rows, columns and boxes,并建立约束。将str的数独输入转化成int类型。声明终止条件sudoku_solved为False并调用backtrack()。
Python版本
回溯法实现
1 |
|
时间复杂性
这里的时间复杂性是常数由于数独的大小是固定的,因此没有 N 变量来衡量。
但是我们可以计算需要操作的次数:\((9!)^9\)。
我们考虑一行,即不多于 \(9\) 个格子需要填。
第一个格子的数字不会多于 \(9\) 种情况,
两个格子不会多于 \(9 \times 8\) 种情况,
三个格子不会多于 \(9 \times 8 \times 7\) 种情况等等。
总之一行可能的情况不会多于 \(9!\) 种可能,
所有行不会多于 \((9!)^9\) 种情况。比较一下:
\(9^{81}\) = 196627050475552913618075908526912116283103450944214766927315415537966391196809 为蛮力法,而 \((9!)^{9}\) = 109110688415571316480344899355894085582848000000000 为回溯法,即数字的操作次数减少了 \(10^{27}\) 倍!
空间复杂性
数独大小固定,空间用来存储数独,行,列和子方块的结构,每个有 \(81\) 个元素。
有效语法糖
python3中global 和 nonlocal 的作用域
1 |
|
利用lambda函数计算变量
1 |
|
Python3的defaultdict
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!