本文是递归系列的第四篇文章.
在前面的递归相关的设计思路, 例题介绍的基础上, 本文通过图文并茂的方式详细介绍三道比较经典的dfs题的思考方向和解题步骤, 以此介绍dfs的一般思路,以及加深对递归设计的认识. 觉得不错就小赞一下啦~
1. 数独游戏
数独游戏大家一定都玩过吧: 简单来说就如下的格子中, 填上剩余空白处的数字1-9,使得每行每列以及所在的小九宫格的所有数字均不同.
我以前并没有玩过数独…也不知道这类题有什么奇技淫巧没, 下面介绍下大概是普通人能够想到思路 :(1a代表左上第一个格子)
-
根据规则,1a不能填3,4,5,7,8. 为了体现规律性, 我们对剩下的可选数字排序, 每次选都从小开始往上挑 — 选1a为1
-
接下来是1b, 选1b为2,符合; 接下来1e为4; 1f6; 1g为8;1h为7;目前有如下结果:
- 现在1i只剩下9可选了,由于7i已经是9,所以该填法出错了… 然后我们拿着小橡皮, 将1h上的7 “擦掉”, 填上剩下的一种可能–9,现在1i只能填7了, 检查一下,完美. 接下来继续是第二行…第三行…
好了, 现在引出今天的主题: dfs(深度优先搜索), 以及 回溯
dfs通俗来讲, 就像小时走大迷宫一样. 遇到岔路口后, 选择其中一条 ,不撞南墙不回头不回头. 遇到尽头后, 回溯 到之前的岔路的位置, 然后选择另一条路径. 如果所有的岔路都试完了均是死路的话, 就说明我正处的这个岔路所在的路径是走错了, 因而就得再一次 回溯 到前一个岔路口, 选择另一个岔路…
抽理一下:
在上述走迷宫中, 站在每一个岔路口时,我们都定义是一种 状态 Si, 当我们(通常按照一定顺序) 选择 某一条路径时 ,:
- 要么是死路, 这时我们需要 回到刚刚的状态 Si(回溯), 选择另一条路径
- 要么达到下一个路口, 就进入了下一个状态 Si+1
而对这个 下一个状态 Si+1 , 我们使用和上述同样的做法 . 这就是DFS的精髓了.
下面继续通过数独题目介绍dfs及其解法思路
输入数独游戏题目, 格式为 9 * 9 的二维数组 ,0 表示未知,其他数字已知
每个零处需填入数字1-9,使得每行 每列 以及 所在的小九宫格 的所有数字均不同.
输入:
005 300 000
800 000 020
070 010 500
400 005 300
010 070 006
003 200 080
060 500 009
004 000 030
000 009 700
下面给出 dfs 思路,
- 定义状态 : 坐标为(x,y), 且需要填数字的格子
- 状态转移: 当前位置填好后, 填它右边最近那个需要填数字的格子, 若是最后一个则提行
- 选择路径顺序(这里是选择数字顺序): 从1~9中选出满足条件的最小的那个, 回溯后, 选倒数第二小的, 依次类推
而通过走迷宫的方法可以看出, 解决Si和解决Si+1的方法相同, 这其实更是个递归问题:
- 找出口: 当遍历到 x = 9时 , 则说明下标为0-8的9行全部填完, 即可退出.
- 找重复: 对每一个状态,判断填入数字的合法规则, 以及选择填入数字的顺序是相同的
- 找变化: 很显然, 每个状态的数组的完成度是不同的, 同时待填入格子的下标也是不同的 .
上述三部曲也是前面提到过的递归设计方法,详情链接:
搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进
好了, 伪代码也能上了:
dfs(table, x, y): # table为当前的数组, x,y为当前状态所需填的格子坐标
# 出口条件
if x == 9:
exit(0)
if table[x][y] == 0: # 如果为0, 表示需要填
for i in range(1, 10): # 选1-9之间的数字放进去, 从小的开始选
flag = checked(table, x, y, i) # 判断是否符合同行同列等
if flag: # 如果满足就填入 i
table[x][y] = i
# 然后转移到下一个状态
dfs(table, x + (y + 1) / 9, (y + 1) % 9)
table[x][y] = 0 # for循环完了, 都不满足, 先将此处恢复成0
# 该层代码 完成, 返回上一层调用 ==> 回溯
else:
# 选择下一个需要处理的位置
dfs(table, x + (y + 1) / 9, (y + 1) % 9)
刚开始学的时候可能对其中核心部分还是有些疑惑:
for i in range (1,10):# 选1-9之间的数字放进去, 从小的开始选
flag = checked(table,x,y,i) # 判断是否符合同行同列等
if flag: # 如果满足就
table[x][y] = i # 填入 i
dfs(table, x + (y+1) /9 , (y+1) % 9) #递归调用 ,转移到下一个状态
table[x][y] = 0 #for循环完了, 都不满足, 先将此处恢复成0
# 函数执行完成, 返回上一层调用处 ==> 回溯
从1-9中选了一个数字, 如果满足, 则填上此数, 同时考察下一个位置 ;
如果不满足, 即flag = false: 就会对1-9中的下一个数进行考察, 如果全都不满足flag = true, 则说明无路可走(死路), 此时需要先将该处恢复成0 , 然后紧接着函数执行完成, 也就返回到上一次调用的地方, 依然在for循环中, 会重新选择上次的数字(比如:上次选了i=5满足, 递归调用后发现下一个位置是怎么填都是死路, 那么回溯后 i 就会继续遍历得到下个满足的数字)
代码如下:
def shudu(table, x, y):
if x == 9 : #此时表明x已经将0-8的9行全部搞定了
print_matrix(table)
exit(0) #找到一个解即可退出
if table[x][y] == 0:
# 选1-9之间的数字放进去
for i in range(1, 10):
flag = checked(table, x, y, i)
if flag:
table[x]