Note:五大算法——回溯算法

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数组去优化,将递归树优化。

reference

labuladong 算法小抄.

发布了28 篇原创文章 · 获赞 2 · 访问量 985

猜你喜欢

转载自blog.csdn.net/sinat_36118365/article/details/104797046