Leedcode day3笔记 回溯算法

回溯算法

回溯算法:一种能避免不必要搜索的穷举式的搜索算法。采用试错的思想,在搜索尝试过程中寻找问题的解,当探索到其一步时,发现原先的选择并不满足求解条件,或者还需要满足更多求解条件时,就退回上一步重新选择,这种走不通就退回再走的技术称为「回溯法」,而满足回溯条件的某个状态的点称为「回溯点」。

回溯算法通常用简单的递归方法来实现,在进行回溯过程中更可能会出现两种情况:

  • 找到一个可能存在的正确答案。
  • 在尝试了所有可能的分布方法之后宣布该问题没有答案。

从全排列问题开始理解回溯算法

以求解[1, 2, 3]的全排列为例请添加图片描述
从全排列的决策树中我们可以看出:

  • 每一层中有一个或多个不同的节点,这些节点以及节点所连接的分支代表了「不同的选择」。
  • 每一个节点代表了求解全排列问题的一个「状态」,这些状态是通过「不同的值」来表现的。
  • 每向下递推一层就是在「可选元素列表」中选择一个「元素」加入到「当前状态」。 当一个决策分支探索完成之后,会逐层向上进行回溯。
  • 每向上回溯一层,就是把所选择的「元素」从「当前状态」中移除,回退到没有选择该元素时的状态(或者说重置状态),从而进行其他分支的探索。

根据上文的思路和决策树,我们来写一下全排列的回溯算法代码(假设给定数组 nums中不存在重复元素)。则代码如下所示:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []    # 存放所有符合条件结果的集合
        path = []   # 存放当前符合条件的结果
        def backtracking(nums):             # nums 为选择元素列表
            if len(path) == len(nums):      # 说明找到了一组符合条件的结果
                res.append(path[:])         # 将当前符合条件的结果放入集合中
                return

            for i in range(len(nums)):      # 枚举可选元素列表
                if nums[i] not in path:     # 从当前路径中没有出现的数字中选择
                    path.append(nums[i])    # 选择元素
                    backtracking(nums)      # 递归搜索
                    path.pop()              # 撤销选择

        backtracking(nums)
        return res

回溯算法三步走

  1. 根据所给问题,定义问题的解空间:要定义合适的解空间,包括解的组织形式和显约束。

解的组织形式:将解的组织形式都规范为⼀个 n n n元组 x 1 , x 2 , . . . x n x_1,x_2,...x_n x1,x2,...xn
显约束:对解分量的取值范围的限定,可以控制解空间的大小。

  1. 确定解空间的组织结构:解空间的组织结构通常以解空间树的方式形象地表达,根据解空间树的不同,解空间分为⼦集树、排列树、 m m m 叉树等。
  2. 搜索解空间:按照深度优先搜索策略,根据隐约束(约束函数和限界函数),在解空间中搜索问题的可⾏解或最优解。当发现当 前节点不满⾜求解条件时,就回溯,尝试其他路径。

如果问题只是求可⾏解,则只需设定约束函数即可,如果要求最优解,则需要设定约束函数和限界函数。

回溯算法的基本思想是:以深度优先搜索的方式,根据产生子节点的条件约束,搜索问题的解。当发现当前节点已不满足求解条件时,就「回溯」返回,尝试其他的路径。
在写回溯算法时,我们可以按照这个思路来书写,具体步骤如下:

  1. 明确所有选择:画出搜索过程的决策树,根据决策树来确定搜索路径。
  2. 明确终止条件:推敲出递归的终止条件,以及递归终止时的要执行的处理方法。
  3. 将决策树和终止条件翻译成代码:定义回溯函数、书写回溯函数主体、明确递归终止条件

回溯算法的应用

子集

题目大意:给定一个整数数组 n u m s nums nums,数组中的元素互不相同。返回该数组所有可能的不重复子集。可以按任意顺序返回解集。
示例

输入 nums = [1,2,3]
输出 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

请添加图片描述
解题思路:可以通过向当前子集数组中添加可选元素来表示选择该元素。也可以在当前递归结束之后,将之前添加的元素从当前子集数组中移除来表示不选择该元素。

from typing import List

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []  # 存放所有符合条件结果的集合
        path = []  # 存放当前符合条件的结果
        def backtracking(nums, index):          # 正在考虑可选元素列表中第 index 个元素
            res.append(path[:])                 # 将当前符合条件的结果放入集合中
            if index >= len(nums):              # 遇到终止条件(本题)
                return

            for i in range(index, len(nums)):   # 枚举可选元素列表
                path.append(nums[i])            # 选择元素
                backtracking(nums, i + 1)       # 递归搜索
                path.pop()                      # 撤销选择

        backtracking(nums, 0)
        return res
        
print(Solution().subsets([1,2,3]))

N 皇后

题目大意:给定一个整数 n n n。返回所有不同的「 n n n皇后问题」的解决方案。每一种解法包含一个不同的「 n n n皇后问题」的棋子放置方案,该方案中的Q和.分别代表了皇后和空位。
示例

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如下图所示,4 皇后问题存在 2 个不同的解法。

请添加图片描述
解题思路:这道题是经典的回溯问题。我们可以按照行序来放置皇后,也就是先放第一行,再犯第二行…直到最后一行。对于 n ∗ n n*n nn的棋盘来说,每一列有 n n n中放法可供选择。我们可以尝试选择其中一列,查看是否与之前放置的皇后有冲突,如果没有冲突,则继续在下一行放置皇后,依次类推,直到放置完所有皇后,并且都不发生冲突时,就得到了一个合理的解。

from typing import List

class Solution:
    res = []
    def backtrack(self, n: int, row: int, chessboard: List[List[str]]):
        if row == n:
            temp_res = []
            for temp in chessboard:
                temp_str = ''.join(temp)
                temp_res.append(temp_str)
            self.res.append(temp_res)
            return
        for col in range(n):							# 枚举可放置皇后的列					
            if self.isValid(n, row, col, chessboard):	# 如果该位置与之前放置的皇后不发生冲突
                chessboard[row][col] = 'Q'				# 选择row,col位置放置皇后
                self.backtrack(n, row + 1, chessboard)	# 递归放置row + 1行之后的皇后
                chessboard[row][col] = '.'				# 撤销选择 row,col位置
                
# 判断当前未知row,col是否与之前放置的皇后发生冲突
    def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]):
        for i in range(row):
            if chessboard[i][col] == 'Q':
                return False

        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if chessboard[i][j] == 'Q':
                return False
            i -= 1
            j -= 1
        i, j = row - 1, col + 1
        while i >= 0 and j < n:
            if chessboard[i][j] == 'Q':
                return False
            i -= 1
            j += 1

        return True

    def solveNQueens(self, n: int) -> List[List[str]]:
        self.res.clear()
        chessboard = [['.' for _ in range(n)] for _ in range(n)]
        self.backtrack(n, 0, chessboard)
        return self.res
        
print(Solution().solveNQueens(5))

电话号码的字母组合

题目大意:给定一个只包含数字2~9的字符串digits。给出数字到字母的映射。注意1不对应任何字母。返回字符串digits在九宫格键盘上所能表示的所有字母组合。
请添加图片描述

示例

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

解题思路:用哈希表保存每个数字键位对应的所有可能的字母,然后进行回溯操作。回溯过程中,维护一个字符串 combination,表示当前的字母排列组合。初始字符串为空,每次取电话号码的一位数字,从哈希表中取出该数字所对应的所有字母,并将其中一个插入到 combination 后面,然后继续处理下一个数字,知道处理完所有数字,得到一个完整的字母排列。开始进行回退操作,遍历其余的字母排列。

from typing import List

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []

        phone_dict = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }

        def backtrack(combination, index):
            if index == len(digits):
                combinations.append(combination)
            else:
                digit = digits[index]
                for letter in phone_dict[digit]:
                    backtrack(combination + letter, index + 1)

        combinations = list()
        backtrack('', 0)
        return combinations

print(Solution().letterCombinations("23"))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值