Key point
解决一个回溯问题,实际上就是一个决策树的遍历过程。
围绕3个问题去展开:
- 路径:指已经做出的选择
- 选择列表:当前面临的选择
- 结束条件:到达决策树底层时,无法再做选择的条件
经典题目
全排列、N皇后问题
回溯算法框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
'''
核心就是for循环中的递归。
在递归之前“做选择”,在递归之后“撤销选择”
'''
全排列问题
知道n个不重复的数,全排列共有 n! 个
注:为了方便理解,全排列问题先不包含重复的数字
当我们对 [1,2,3] 进行全排列时,首先想到的的是穷举,大致流程图如下:
先固定第一位,然后接着选择第二位、第三位。展开后就是一颗决策树。而上面说过,回溯问题就是一个决策树遍历的问题。
如果用上面的框架解释:图中红色2就是【路径】,记录已经做过的选择;[1,3] 就是【选择列表】,表示你当前可以做出的选择;【结束条件】是遍历到树的叶子节点,就是选择列表为空的时候。
此处定义的backtrack函数可以看做是指针,在这颗树上游走,同事要正确维护每个节点的属性,每当走到树的底层时,其【路径】就是一个全排列。
- 决策树上的游走可以采用前序遍历和后序遍历
- 前序遍历:在进入某一个节点之前的那个时间点执行
- 后序遍历:在离开某个节点之后的那个时间点执行
【路径】和【选择】作为每个节点的属性,在执行前序后序遍历时,要做出相应的动作—— 做选择 和 撤销选择
代码
class Solution:
def permute(self, nums):
'''
路径:存放在track中
选择列表:nums中不在track的元素
结束条件:nums中的元素全部在track中出现
'''
def backtrack(nums, track):
# 触发结束条件
if len(nums) == len(track):
res.append(track[:])
return
for i in range(len(nums)):
# 排除不合法的选择
if nums[i] in track:
continue
# 做选择
track.append(nums[i])
# 进入下一层决策树
backtrack(nums, track)
# 撤销选择
track.pop()
res = []
track=[]
backtrack(nums, track)
return res
note: 代码重在理解框架,效率不会很高。但不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
N皇后问题
题目:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。(皇后可以攻击同行、同列、同一条斜线)
如图为N=8的放置方法。
用回溯框架去看,将棋牌比作一个决策树。
- 决策树的每一层表示棋牌上的每一行;
- 每个节点做选择 就是在该行的任意一列放置一个皇后。
- 当行超过棋盘最后一行就结束
代码
class Solution(object):
def solveNQueens(self, n):
'''
"."表示空,‘Q’表示皇后,初始化空棋盘
'''
board = [['.']* n for _ in range(n)]
res = []
'''
路径:board 中小于row的那些行都已经成功放置了皇后
选择列表:第 row 行的所有列都是放置皇后的选择
结束条件:row 超过 board 的最后一行
'''
def backtrack(board , row):
# 触发结束条件
if row == len(board):
tem_list = []
for each_row in board:
tem = "".join(each_row)
tem_list.append(tem)
res.append(tem_list)
return
n = len(board[row])
for col in range(n):
# 排除不合法选择
if not isValid(board,row,col):
continue
# 做选择
board[row][col] = 'Q'
# 进入下一行的决策
backtrack(board,row+1)
# 撤销选择
board[row][col] = '.'
def isValid(board,row,col):
n = len(board)
# 检查同列中皇后是否冲突
for i in range(n):
if board[i][col] == 'Q':
return False
# 检查左上的对角线皇后是否冲突
for i,j in zip(range(row-1,-1,-1),range(col-1,-1,-1)):
if "Q" in board[i][j]:
return False
# 检查右上的对角线皇后是否冲突
for i,j in zip(range(row-1,-1,-1),range(col+1,n)):
if "Q" in board[i][j]:
return False
return True
backtrack(board,0)
return res
动态规划需要明确知道 【状态】【选择】和【base case】
而回溯算法需要知道【路径】,【当前选择列表】和【结束条件】
一定程度上动态规划的暴力求解阶段就是回溯算法。只是在求解过程中的一些重叠子问题,可以用dp数组去优化,将递归树优化。