框架妙解回溯算法

例题:46.全排列 51.N皇后
回溯算法的框架:
解决一个回溯问题,需要思考三个方面:

  • 路径:已经做出的选择
  • 选择列表:当前可以做的选择
  • 结束条件:到达决策树底层,无法再做选择的条件

代码框架

resul=[]
def backtrack(路径,选择列表):
	if 满足结束条件:
		result.append(路径)
		return
	for 选择 in 选择列表:
		做选择
		backtrack(路径,选择列表)
		撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后【撤销选择】,特别简单。

一、全排列问题

排列组合:n 个不重复的数,全排列共有 n! 个
对于[1,2,3]穷举:
先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……
这就是回溯算法,回溯树如下:

只要从根节点遍历这棵树,记录路径上的数字,就是所有的全排列,这颗树可以称为回溯算法的[决策树]
为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:

现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。
对照之前的框架:[2]是【路径】,记录已做过的选择;[1,3]是【选择列表】,表示当前可以做出的选择;【结束条件】就是遍历到树的底层,在这里就是选择列表为空时。
可以把「路径」和「选择」列表作为决策树上每个节点的属性:

我们定义的 backtrack 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列。
各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:

def traverse(root):
	for (child:root.childern):
		# 前序遍历需要的操作
        traverse(child);
        # 后序遍历需要的操作	

前序遍历和后序遍历,是两个很有用的时间点

前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。
回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:

for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表移除
    路径.append(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。

  • 代码如下:

class Solution:
	def __init__(self):
		self.res = []
		self.track = []
	def permute(self,nums):
		self.backtrack(nums)
		return self.res
	# 路径:记录在 track 中
	# 选择列表:nums 中不存在于 track 的那些元素
	# 结束条件:nums 中的元素全都在 track 中出现
	def backtrack(self,nums):
		# 结束条件
		if len(nums) == len(self.track):
			self.res.append(self.track[:])
			return
		for i in range(len(nums)):
			# 排除不合法的选择
			if nums[i] in self.track:
				continue
			# 做选择
			self.track.append(nums[i])
			# 进入下一层决策树
			self.backtrack(nums)
			# 取消选择
			self.track.pop()

print(Solution().permute([1, 2, 3]))

二、N皇后问题

题目描述:
给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
函数 backtrack 依然像个在决策树上游走的指针,通过 row 和 col 就可以表示函数遍历到的位置,通过 isValid 函数可以将不符合条件的情况剪枝:

  • 代码如下
'''
N皇后问题
'''

class Solution:
    def NQueens(self, n):
        # 创建n×n的棋盘
        grid = [['.'] * n for i in range(n)]
        # 存储皇后的位置索引
        queen = set()
        # 存储最后的结果
        res = []
        print(grid)
        self.backTrack(grid, 0, n, queen, res)
        return res

    def backTrack(self, grid, index, n, queen, res):
        # 当前皇后的个数是否等于 n 了,等于的话就加到结果中
        if index == n:
            solution = []
            for _, col in sorted(queen):
                solution.append('.' * col + 'Q' + '.' * (n - col - 1))
            res.append(solution)
        for i in range(n):
            if (self.is_attack(grid, index, i)):
                # 进行选择
                queen.add((index,i))
                grid[index][i] = 'Q'
                # 递归遍历
                self.backTrack(grid, index + 1, n, queen, res)
                # 撤销选择
                grid[index][i] = '.'
                queen.remove((index,i))

    # 判断是否合法:列;主对角线;副对角线是否符合要求
    # 其中主对角线位置等于当前位置的行减一,列减一。
    # 副对角线位置等于当前位置的行减一,列加一。
    def is_attack(self, gird, row, col):
        # 纵向合法性校验
        for i in range(row):
            if gird[i][col] == 'Q':
                return False
        # 主对角线合法性校验
        x = row - 1
        y = col - 1
        while x >= 0 and y >= 0:
            if gird[x][y] == 'Q':
                return False
            x -= 1
            y -= 1
        # 副对角线合法性校验
        x = row - 1
        y = col + 1
        while x >= 0 and y <len(gird[0]):
            if gird[x][y] == 'Q':
                return False
            x -= 1
            y += 1
        return True


print(Solution().NQueens(10))

回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:

def backtrack(...):
    for 选择 in 选择列表:
        做选择
        backtrack(...)
        撤销选择

写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。

其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?

某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年5月16日
下一篇 2022年5月16日

相关推荐