题目链接:https://leetcode-cn.com/problems/eight-queens-lcci/
八皇后问题是典型的回溯法的应用,而回溯法的本质就是树的遍历和剪枝,N皇后问题可以说是N叉树的遍历和剪枝问题。对于树的遍历问题都是能抽象出解题模板的,见后续分析。完整代码在文章末尾。这里主要详细记录一下对于这种困难题怎么一步一步把代码写出来,写的会很细。
回溯法一般都是可以通过递归来实现;设计一个递归程序需要明确这几点:1、递归的终止条件是什么?2、递归如何传值?传的值的含义是什么?3、如何设计具体的递推过程能够达到我们的目标?
一般而言,对于这种比较复杂的问题,我们是很难直接写出完整的代码的;遇到这种问题,我们可以先把问题求解框架写出来,明确解题框架大概需要些什么模块,然后在去完善每个模块。下面我们按照这种思路来解决这道题。
由于我们打算采用回溯法解决这道题,那么肯定需要设计一个递归函数;八皇后问题的一个关键点就在于判断后续皇后摆放的位置是否与之前皇后摆放的位置相冲突,所以还需要一个判断是否冲突的函数;接下来我们把整体框架写一写。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
self.res = []#保存摆放位置的列表
self.n = n
return self.res
def recur(self):#递归函数;如何去设计递归函数?
pass
def check(self):
pass
接下来我们把重心放在递归函数的设计上:1、终止条件是什么?2、怎样传值?3、具体递归流程
假设我们当前皇后的摆放位置在(row, col),如果当前皇后摆放位置和之前皇后已经摆放的位置不冲突并且没有达到终止条件,则我们需要继续递归下去(这里隐含着一个事实:即之前摆放的位置都是合法的);从这里可以发现,我们需要设计两个元素:1、之前已经摆放的位置;2、递归终止条件。所以这里我们知道recur这个递归函数至少需要传三个值:当前摆放位置的坐标row, col,之前摆放的位置pre_state(具体怎么设计它暂时不考虑)。于是我们可以把recur函数完善一点了。
def recur(self, row, col, pre_state):
pass
接下来是如何设计终止条件?
这样想:如果我们已经递归到最后一行了,比如对于八皇后问题而言,此时recur(row,col,pre_state)中的row=7,此时最后一行上是没有任何皇后的(因为我们是按行来进行递归的,这样可以保证每行只有一个皇后;同理可以按照列来进行递归),这个时候我们还需要进入这个递归函数,判断(row=7, col)的摆放位置是否和之前冲突了,然后继续递归调用recur(row+1, col)(记住我们是行优先进行递归的),这时的递归调用row == 8了,越界了,但同时也说明之前的摆放位置也是合理的,好,这就得到我们的终止条件:即
def recur(self, row, col, pre_state):
if row == n:
pass#要进行什么操作先不写
return
recur(...)#没有到达终止条件,继续递归
我们再看,达到递归终止条件后要做什么操作。当达到终止条件时,说明之前的摆放位置是合法的,即可以把这种摆放方案添加到我们的结果里面了,代码如下:
def recur(self, row, col, pre_state):
if row == n:
self.res.append(ans)#这里的ans即合法的摆放方案,具体怎么得到ans先不管
return
#接下来就是设计如何递归了。由于是行优先,所以我们对下一行row+1递归,但是列有n种选择,所以代码为:
#这里我们需要知道n值,所以在写到这里的时候,在solveNQueens中添加self.n = n;这就是我所说的可能
#一开始你不知道要写什么代码,先把框架搞好,后续完善的时候你就知道要写些什么代码了。
for j in range(self.n):
self.recur(row+1, j, pre_state)
#写到这,我们又需要考虑了,两次递归之间怎么传值?我们看到row+1了,那么pre_state是不是也需要变化呢?
#并且回溯法的框架是这样的:
"""
做出选择
进入下一层递归
撤销选择
"""
#所以我们可以尝试着这样写:
for j in range(self.n):
#做出选择
pre_state.append((row+1, j))#含义是我们选择在(row+1, j)摆放皇后,然后在下一轮递归判断这个位置是否合法,所以我们需要把(row+1, j)添加进pre_state;
self.recur(row+1, j, pre_state)#下一层递归
pre_state.pop()#撤销选择,即(row+1, j)位置不摆放皇后了
return
#好,写到这我们认为递归函数差不多写完了(如果后续发现不够完善可以回头在来修改)
#写到这,发现这个递归函数里面怎么没有判断位置是否冲突的函数,所以应该是遗漏了,继续完善
#如何才能进入下一轮递归?即recur(row, col, pre_state)中的(row, col)不与pre_state冲突才行,所以:
def recur(self, row, col, pre_state):
if row == self.n:
self.res.append(ans)
return
if self.check(row, col, pre_state):#不冲突,开启下一轮递归
#所以这里添加一句
if row == self.n - 1:
self.recur(row+1, col, pre_state)
return
pre_state.append((row+1, j))#其实这里有个问题,如果row==self.n-1,那么row+1=self.n了,在check函数会出现越界的错误(其实自己开始写的时候也没考虑到这个问题,也是从报错中发现这个问题的;代码水平还是不够
self.recur(row+1, j, pre_state)
pre_state.pop()
接下来看check函数怎么写?
这里以八皇后为例,我们看一看八皇后的棋盘有什么样的规律?
好,明白这两个规律后开始写check函数:
def check(self, row, col, pre_state):
#这里我们只需要判断列是否冲突,两个对角线是否冲突,行不用判断;因为我们行优先进行递归,每行必然只有一个皇后;这里要注意一点:pre_state中是包含了(row, col)的,从recur函数中可以看出来。而我们是要判断(row, col)是否和之前的位置冲突,因此遍历的时候我们只遍历pre_state[:-1].
for i, j in pre_state[:-1]:
if j == col:#说明列已经有皇后
return False
elif (i - j) == (row - col):#'\'对角线上有皇后了;这里就利用到了棋盘的规律了
return False
elif (i + j) == (row + col):
return False
return True
好了,写到这,两个最主要的模块recur()函数和check()函数我们都写好了。剩下的代码就不难写了,这里直接贴出完成代码:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
pre_state = []
self.res = []
self.n = n
for i in range(n):
pre_state.append((0, i))
self.recur(0, i, pre_state)
pre_state.pop()
return self.res
def recur(self, row, col, pre_state):
if row == self.n:
board = [['.']*self.n for _ in range(self.n)]
for i, j in pre_state:
board[i][j] = 'Q'
ans = [''.join(item) for item in board]
self.res.append(ans)
if self.check(row, col, pre_state):
if row == self.n - 1:
self.recur(row+1, col, pre_state)
return
for j in range(self.n):
pre_state.append((row+1, j))
self.recur(row+1, j, pre_state)
pre_state.pop()
return
def check(self, row, col, pre_state):
for i, j in pre_state[:-1]:
if j == col:
return False
elif (row - col ) == (i - j):
return False
elif (row + col) == (i + j):
return False
return True