案例:
八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当 n = 1 或 n ≥ 4 时问题有解。
分析一
关于皇后冲突的判定
用自然语言很容易描述八个皇后的位置制约关系,即棋盘的每一行,每一列,每一个条正斜线,每一条反斜线,都只能有1个皇后。如果用这个方法,判断新加入的皇后位置是否与已经存在的皇后位置冲突,先求出新皇后在哪一行,列,正反两条斜线上,再依次判断是否冲突。我们先用图表对棋盘进行分析。
其中,每一行只能有一个皇后,且列不能重复。不管是“/”还是“\”,横纵坐标之差的绝对值即为行序号或列序号之差。
对上述判定进行程序化,如下所示:
#冲突检查,在定义state时,采用state来标志每个皇后的位置,其中索引用来表示横坐标,基对应的值表示纵坐标,例如: state[0]=3,表示该皇后位于第1行的第4列上
def conflict(state, nextX):
nextY = len(state)
for i in range(nextY):
#如果下一个皇后的位置与当前的皇后位置相邻(包括上下,左右)或在同一对角线上,则说明有冲突,需要重新摆放
if abs(state[i]-nextX) in (0, nextY-i):
return True
return False
递归函数
#采用生成器的方式来产生每一个皇后的位置,并用递归来实现下一个皇后的位置。
def queens(num, state=()):
for pos in range(num):
if not conflict(state, pos):
print(state,pos)
#产生当前皇后的位置信息
if len(state) == num-1:
yield (pos, )
#否则,把当前皇后的位置信息,添加到状态列表里,并传递给下一皇后。
else:
for result in queens(num, state+(pos,)):
print('result:',end='')
print(result)
yield (pos, ) + result
为了便于理解,在程序中加了输出语句,执行 print(list(queens(8,(0, 4, 7, 5))))
这里我们尝试对输出结果进行分析。
(0, 4, 7, 5) 2
(0, 4, 7, 5, 2) 6
(0, 4, 7, 5, 2, 6) 1
(0, 4, 7, 5, 2, 6, 1) 3
result:(3,)
result:(1, 3)
result:(6, 1, 3)
(0, 4, 7, 5) 3
(0, 4, 7, 5, 3) 1
(0, 4, 7, 5, 3) 6
[(2, 6, 1, 3)]
先大概说下 yield,它类似于 return,但和 return 不同的是 return 返回一个值(这个“值”可以是数值,字符串,序列等,但只是一次一个),然后函数就结束了,而 yield 某个值后函数不会结束,而是继续找出接下来所有符合条件的值,然后才结束。
根据结果可知,当 state 中值为(0,4,7,5)时,第一次进入递归函数中,发现 pos=2 通过判定检查,但是由于 len(state) 长度不满足条件,则继续进入到递归函数,第二次发现 pos=6 符合,接着继续进行递归,第三次发现 pos=1 符合,接着进行递归,第四次发现 pos=3 复合,此时满足 len(state)==7条件,返回值 (3,) ,第四次递归函数返回 (3,)。则 for result in queens(num, state+(pos,))
语句第一次输出便为 (3,);同时返回 (1,3) 值,第三次递归函数返回 (1,3),此为 for 循环的第二次输出;同理,第二次递归函数返回 (6,1,3)。此时 pos=2,(2,6,1,3)为一个结果,但是还未结束。回溯到第一次递归函数后,继续进行判断,发现 pos=3 也通过判定检查,同上述一样,一直到第四次递归函数中,没有找到符合要求的值,所以层层向上返回时,未得到结果 。最终结果为 (2,6,1,3)。
这里只是针对这种给定数据的情况进行分析,实际过程中会更加复杂,但是希望能通过上述分析,对代码部分不再迷惑。
总结:DFS 的重要点在于状态回溯,当没有找到目标值,就会返回上一级,再接着查找是否有合适的值。
分析二
首先列出代码,然后再进行分析。
n = 8
v = [[0] * 8 for x in range(9)]#棋盘列表
temp = [0]*9 #标志每个皇后的位置,其中索引用来表示横坐标,基对应的值表示纵坐标,例如:temp[0]=0表示该皇后位于第一行第一列上
def conflict(num):
for i in range(num):
# 判断列和对角线,如果第num+1行上皇后的位置的列数与之前的皇后所在位置的列数相同,又或者当前皇后的位置的列数与之前皇后位置列数做差的绝对值等于行数做差,则说明有冲突,需要重新摆放
if (temp[num] == temp[i] or abs(temp[i] - temp[num]) == num - i):
return False
return True
def print_v(v):
#输出八皇后列表
for i in range(n):
print(v[i])
def dfs(num):
if num >=n:
print_v(v)
print('***********' * 5)
for i in range(n):
temp[num] = i #当前皇后的位置设定为第num+1行,第i+1列
if v[num][i] != 1 and conflict(num):
v[num][i] = 1
dfs(num+1)
v[num][i] = 0
if __name__ == '__main__':
dfs(0)
逻辑分析:
递归指的是 dfs()方法,目标为 if num >=n: print_v(v)
,无路可走指的是 if v[num][i] != 1 and conflict(num)
条件不通过,回溯指的是 v[num][i] = 0
。
首先明确上述内容,我们再来研究代码部分。其实和分析一相似(毕竟都是 DFS 算法),当从第一行开始给定皇后的位置,然后深处查找第二行、、、一直到第八行,当第八行数据找到之后,我们看到会继续进入 dfs() 方法中,此时 num 值为 8,找到目标,但是程序会继续找下去,但是if v[num][i] != 1 and conflict(num)
判定过不去,所以会向上回溯,将第八行的皇后位置置为 0,再查找是否存在第二个合适的位置,没有则继续向上回溯,将第七行的皇后位置置为 0,然后查找合适的位置,以此类推,直到找到所有的值集。
这里声明一下 v 和 temp 的大小设定为 9 的原因,主要是因为找到目标后,代码还会往下执行,temp[num] 和 v[num][i] 还需要进行冲突判定校验,为了避免程序报错,需要这样声明。
分析三
n = 8
results = []#一组解
ans = [] #一个解,每个元素的索引值为皇后所在的行数,元素值为皇后所在的列数
def conflict(k):
'''
冲突检查
:param k: 第k行,即ans中第k个元素
:return:
'''
if len(set(ans)) != k+1:#皇后所在列数不允许存在相同的情况
return False
for i in range(k):
if abs(ans[k]-ans[i]) == abs(k-i):#当前皇后的位置的列数与之前皇后位置列数做差的绝对值等于行数做差,则说明有冲突,需要重新摆放
return False
return True
def dfs(k):
global results
if k >= n:
results.append(ans[:])
else:
for i in range(n):
ans.append(i)
if conflict(k):#剪枝
dfs(k+1)
ans.pop()#回溯
def show(ans):
for i in ans:
print('0'*i+'1'+'0'*(n-i-1))
dfs(0)
print(results)
show(results[0])
对于八皇后问题的求解,前两种方法都较为复杂,不易于理解(当时花费很多时间分析代码运行流程)。分析三是在学习回溯法详解后,根据自己的理解构建的代码,更加直观。