关于N皇后问题。它也是一个回溯问题。为何?因为它也符合类似”深度优先搜索“的树形思路。
例如,以4皇后为例,4*4的棋盘,假设我们从第一行开始算起,我们会发现,如果你选择第一行第一列(0, 0),那么一次为出发点,你可以去尝试接下来第二行的所有位置(1, 0) ~ (1, 3);假设你第二行选择了合适的放置位置(1, 2),那么你可以接着以[(0,0), (1,2)]为出发点考虑第三行所有位置是否放置皇后合适。
显然,每个合适的皇后放置点,都是一个树形节点的开始,如下图所示:
0,0 | 0,1 | 0,2 | 0,3 |
---|---|---|---|
1,0 | 1,1 | 1,2 | 1,3 |
2,0 | 2,1 | 2,2 | 2,3 |
3,0 | 3,1 | 3,2 | 3,3 |
“ 我是棋盘。。。”
如图所示,这显然是个可以用递归处理的回溯思想/深度优先搜索思想解决的问题。所以提到回溯,我们的大框大概就架好了:
is_valid()
成立,col仍要步进的情况)class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:# 主函数,准备递归物料、递归函数入口
self.res = [] # 答案记录器
self.n = n # 方便会用到的子函数
tmp = [None for _ in range(self.n)] # 递归复用数组,用一维数组记录Queen位置,数组的index就是row,index对应的元素就是col
self.backtrace(tmp, row=0)
return self.return_result()
def backtrace(self, tmp, row):
if row == self.n: # 如果已经判断完全部的行了,就返回并记录吧
self.res.append(tmp[:]) # 这里必须是tmp的切片,因为python切片是浅拷贝,浅拷贝的结果不会在后续递归中被修改掉
return # 这里必须有一个return,因为当row == self.n成立时,这条递归之路即该完美结束
col = 0 # 每一行的循环判断,都从0位置开始
while col < self.n:
if self.is_valid(tmp, row, col): # 判断当前位置是否可放Queen
tmp[row] = col # 若可以,则记录于tmp
self.backtrace(tmp, row + 1) # 然后开启下一行的递归判断(row+1)
col += 1 # 不论是否成立,都应将col的位置步进(col+1)
return # 这里必须有一个return,因为当while循环跳出,说明本行所有col都不能放Queen,应发生回溯,目前的递归之路应提前结束
def is_valid(self, tmp, row, col):
for i, j in enumerate(tmp):
# 在判断Queen是否可放置的时候,只有当前行之前的Queen值得被考虑。由于tmp是复用的,所以当前row及其之后的row都有可能已有无用记录
if i == row:
break
if j == col or row - i == abs(col - j): # 两个不可放Queen条件:1.在同列;2.在**左、右**两侧对角线。
return False
return True # 全部经历过考验,则可以放置
def return_result(self): # 将结果转换成LeetCode要求的样子
saver = []
for arr in self.res:
solve = []
for pos in arr:
line = ["."] * self.n
line[pos] = "Q"
solve.append(''.join(line))
saver.append(solve)
return saver
代码如上,思路很清晰,比我原本的分析只多了个LeetCode所需答案的生成函数 return_result()
,但有些问题还是指的讨论的:
tmp
数组的复用这也是我之前被卡壳的地方——那就是,在递归过程中,如何在每一次递归过程中都生成一个空list存放此次递归计算出的结果?或者说,code中所述的tmp
数组,如何在每一次递归过程中复用?说实话,我觉得这应该算递归的普遍性问题,所以值得思考一下。
对于回溯问题,这种树形思维下的递归问题,最好的方法肯定是数组复用,因为树形的行走方式,注定数组一般而言是可以复用的。那么Python实现里如何复用,以及这么写为何可以复用就是个针对回溯的coding问题。
首先,若想复用数组,就当然是从递归入口处传入复用的数组。对于本题而言,数组长度就是棋盘size n
。如code line-5,我们便建立了这样一个数组。因为在回溯的树形思维中,任何一条递归路走不通或走到底有结果的时候,我们都应将之终止,终止后会回到发生递归的地方(code line-17),row相对于之前的递归回退一步。而tmp
由于是复用的,所以会从row对应的index处开始重新更新,所以递归函数是这样复用数组的。
其次,对于结果的记录,如code line-11,我记录的是tmp
的切片。因为若仅记录tmp
,那么在后面复用的过程中,tmp
一被改变,self.res
中记录的tmp
也会变化。而Python切片是浅拷贝,而self.res
中没有可变对象,所以就隔离了后序递归中对tmp
的修改。
深度优先搜索
的树形思路”?以第LeetCode-17题为例,该题让写出2-9数字对应的字母所有组合。
随便举个例子——2,3,4。对于2,3,4,这三个数字各自分别对应3个字母,假设从数字2对应的a出发,到数字3,它又会有3条选择(d,e,f),任选一个,到了4又是三个选择,情况如下:
回溯问题的通用代码框架
class Solution:
def lcb(self, digits: str):
self.res = []
self.backtrace()
return self.res
def backtrace(self, ):
if self.valid():
return xxx or self.res.append(xxx)
else:
for i in arr:
self.bc(i-1)
def valid():
return True
class Miner:
def lcb(self, digits: str):
if not digits:
return []
phone = {'2':['a','b','c'],
'3':['d','e','f'],
'4':['g','h','i'],
'5':['j','k','l'],
'6':['m','n','o'],
'7':['p','q','r','s'],
'8':['t','u','v'],
'9':['w','x','y','z']}
str_list = [phone[d] for d in digits]
self.res = []
self.bc('', str_list)
return self.res
def bc(self, prefix, str_list):
if not str_list:
self.res.append(prefix)
else:
for j in str_list[0]:
self.bc(prefix + j, str_list[1:])