问题描述
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。
回溯
回溯的思想非常简单,就像我们走迷宫,如果当前位置的下一步有三种走法,随机选择一个方向,如果还可以继续走下去,就一直往深处走,否则就回退到当前位置,在另外两条路中选择一条继续探索,整个思想可以抽象为树和图的遍历,以N-皇后问题
N
=
4
N=4
N=4为例,整个的搜索空间为
4
∗
4
∗
4
∗
4
=
256
4*4*4*4=256
4∗4∗4∗4=256,但是通过皇后吃皇后的规则,可以对搜索空间进行剪枝,以达到减少解空间的目的。
上图中的左图是所有解空间,通过添加剪枝条件,如上图的右图,在第一行,第一列放入皇后后,在第二行第一列放皇后时考虑到两个皇后同列了,此时可以将该可能解以及下面的所有可能解都剪掉,回溯到上一个节点,即第一行第二列,重新考虑第二行皇后的位置。
代码实现
经过上面的分析,可见代码主要有两个核心模块:
- 剪枝部分:判断皇后放在这个位置是否是安全的
- 回溯递归部分:遍历所有可能情况
剪枝部分的代码只要理清了规则并不难:
递归版本
class N_Queue():
def __init__(self,N):
self.N = N
self.board = [] # 记录棋盘状态
self.sol=0 # 记录解法总数
def isSafe(self,col):
row,col = len(self.board),col # 当前栈内存储的元素个数,表明从上到下考虑到第几行
# row,col:需要判断是否安全的皇后的位置
# 判断是否同一列
if col in self.board:
return False
# 判断所有对角线
for i in range(len(self.board)):
j = self.board[i]
if abs(i-row)==abs(j-col):
return False
return True
对于回溯部分,因为类似于树的遍历,首先写了这样的一个版本:
def putQueue(self,col):
if len(self.board)==self.N: #行数=N,已经安全放完皇后,是为一种解法
self.sol+=1 # 解法+1
print("solution %d"%self.sol) #解法输出
print(self.board)
col = self.board.pop(-1) # 回退到上一步,考虑是否还有其他解
self.putQueue(col+1) # 回退后继续探索
return
if col==self.N: # 列以搜索完
if len(self.board)==0: # 如果此时栈为空,搜索完所有可能解,整个程序结束,返回
return
else:
col = self.board.pop(-1) # 回退上一步
self.putQueue(col+1) # 搜索下一列
return
if self.isSafe(col): # 列还未搜索完,且位置安全
self.board.append(col) # 放皇后
self.putQueue(0) # 搜索下一行
return
else:
self.putQueue(col+1) # 列还未搜索完,且位置不安全,搜索下一列
return
上面的代码虽然解决四皇后五皇后问题,但是在N=6时就报超过最大递归深度了python在递归中的坑, 最后参考他人的成果,将putQueue函数改成如下情况:
def putQueue_update(self,row):
if row==self.N:
self.sol+=1
print("solution %d"%self.sol)
print(self.board)
else:
for i in range(self.N):
if self.isSafe(i):
self.board.append(i)
self.putQueue_update(row+1)
self.board.pop(-1)
八皇后也能完美解决了,那么第一版为什么一直递归深度超限呢?
第一版的一次递归只考虑了一个点的情况,此时考虑这一个点的回溯条件就比较复杂:是行搜索完了?还是列搜索完了?栈中要堆入所有点的putQueue()函数,每个函数中还有很多的push、pop操作,这就导致栈非常深。
而第二版一次递归考虑了一行所有列的情况,且通过一个pop()函数清理了所有
t
+
1
t+1
t+1轮递归对棋盘的操作,
t
+
1
t+1
t+1轮操作完成后,会回到
t
t
t轮考虑之后的列情况,这个逻辑(递归函数返回后程序会怎么运行)也是经常被我忽略的地方。
非递归版本
递归的代码虽然比较好理解,但是在效率上却是次优的,那么怎么将递归的代码转换为非递归的代码呢?递归的实质是通过函数将当前的状态作为参数传递给函数本身,然后对这些状态参数做一样的判断和处理,那么转化为非递归版本时,最简单的方法就是找一个数据结构来存储下一个状态参数,每次读取状态参数并进行处理就可以了,而且这个数据结构在本次状态全部处理完成后返回上一个状态而不是最先的状态,所以栈是最合适的。
def putQueue_nonRecursive(self):
stack = [] # stack用来存储下一次遍历的节点
stack.append((0,0)) # 初始为(0,0)
while len(stack)>0:
row,col = stack.pop(-1)
if col==self.N and len(stack)==0: #遍历为所有节点
break
if col==self.N: #列遍历完,返回上一行,此时需要将board中的上一行的皇后位置抹掉
self.board.pop(-1)
continue
if self.isSafe(col): # 如果该节点是安全的
self.board.append(col) # 将位置记录到board中
if len(self.board) == self.N: #如果board的所有行都记录了,输入该种皇后摆法
self.sol+=1
print("solution %d"%self.sol)
print(self.board)
stack.append((row,col+1)) # stack记录下次遍历的节点,列优先
stack.append((row+1,0)) # 行
else:
stack.append((row,col+1)) # 位置不安全,继续列搜索