回溯法详解

一、模板格式

  回溯法问题实际上是一个决策树的遍历过程。可以分为三个部分:

  1、路径:也就是已经做出的选择。

  2、选择列表:也就是当前可以做的选择。

  3、结束条件:也就是到达决策树底层,无法再做选择的条件。

  回溯法不好理解的地方应该在撤销选择这一步,回溯会沿着一条路径走到结束状态,到这一步之后,需要返回到上一状态,这时候就需要执行撤销操作。

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

二、示例讲解

1、全排列

"""
给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

"""


class Solution:
    def permute(self, nums):
        result = []
        path = []
        self.back_trace(nums, result, path)
        return result

    def back_trace(self, nums, result, path):
        """
        用递归来回溯
        :param nums:
        :param result:
        :param path:
        :return:
        """
        if len(path) == 3:  # 满足结束条件
            result.append(path[:])
            return
        for i in range(len(nums)):
            path.append(nums[i])  # 做选择
       # 全排序中可选择的列表是除了当前节点外的所有节点,所以是num[:i] + nums[i+1:] self.back_trace(nums[:i]
+ nums[i+1:], result, path) path.pop() # 撤销选择

2、全排列II

  在包含重复数字的情况下,返回不重复的全列表。核心在于如何去重,即遍历是做剪枝操作,减少复杂度。为确保重复的数字不会重复组合,则需要对每个重复的数字标记一个顺序,并保证他们的相对位置不变,举例对于3个1,可以标记为1a、1b、1c。如果执行时1a没有被选择的话1b是不可以被选择的。这样就能避免重复。所以第一步对数组排序,确定好重复的数字的顺序。"""给定一个可包含重复数字的序列,返回所有不重复的全排列示例:


输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

"""


import copy


class Solution:
    def permuteUnique(self, nums):
        result = []
        path = []
        nums.sort()  # 确定好重复数字的顺序
        self.back_trace(nums, result, path, len(nums))
        return result

    def back_trace(self, nums, result, path, length):
        if len(path) == 3:
            result.append(path[:])
            return
        for i in range(len(nums)):
       # 这一步是关键,以[1, 1, 2]为例,第一步选择第一个 1 之后,因为i = 0不会触发continue,而剩余的[1,2]没有重复,所以会得到[1] + [1, 2] = [1, 1, 2]
       # 和[1] + [2, 1] = [1, 2, 1]。当遍历到第二个 1 时满足下面的条件会触发continue,所以会直接跳到 2,而剩余的[1, 1]有重复,和上面一样分析只会得到[1, 1],
       # 最终就是 [2] + [1, 1] = [2, 1, 1]
if i > 0 and nums[i] == nums[i-1]: continue path.append(nums[i]) self.back_trace(nums[:i] + nums[i+1:], result, path, length) path.pop()

3、子集

  子集和全排列有两个不同的地方,一是没有终止条件,即所有状态下的结果都要;二是有顺序的,不可扰乱。

"""
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

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

"""


class Solution:
    def subsets(self, nums):
        result = []
        path = []
        self.back_trace(nums, result, path)
        return result

    def back_trace(self, nums, result, path):
        result.append(path[:])  # 保存任意状态下的路径
        for i in range(len(nums)):
            path.append(nums[i])
       # num[i+1:] 表示不可扰乱数组的顺序,只能取当前状态后的元素 self.back_trace(nums[i
+1:], result, path) path.pop()

4、子集II

  同全排列II,对重复的元素排序,保证它们之间的相对位置。

"""
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

"""


class Solution:
    def subsetsWithDup(self, nums):
        result = []
        path = []
        nums.sort()  # 一定要排序,因为是要按照后面是否和前面重复来筛选的,所以要将相同的元素排列在一起
        self.back_trace(nums, result, path)
        return result

    def back_trace(self, nums, result, path):
        result.append(path[:])
        for i in range(len(nums)):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            path.append(nums[i])
            self.back_trace(nums[i + 1:], result, path)
            path.pop()

5、组合

  组合中要注意三个点:一是终止条件是选择的元素相加等于target;二是如果当前选择的元素和大于target,需要剪枝。三是每个元素可以重复使用。

"""
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。 
示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]
示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

"""


class Solution:
    def combinationSum(self, candidates, target: int):
        result = []
        path = []
        self.back_trace(candidates, result, path, target)
        return result

    def back_trace(self, nums, result, path, target):
     # 满足终止条件
if sum(path) == target: result.append(path[:]) return for i in range(len(nums)):
       # 满足剪枝的条件
if sum(path) + nums[i] > target: continue path.append(nums[i])
       # num[i:] 是确保元素可以重复使用,不同于子集中的num[i+1:] self.back_trace(nums[i:], result, path, target) path.pop()

6、组合总数II

  重复元素的问题同上全排列II和子集II。

"""
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。 
示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

"""


class Solution:
    def combinationSum2(self, candidates, target: int):
        result = []
        path = []
        candidates.sort()  # 排序保证重复元素的相对位置
        self.back_trace(candidates, result, path, target)
        return result

    def back_trace(self, nums, result, path, target):
        if sum(path) == target:
            result.append(path[:])
            return
        for i in range(len(nums)):
# 多增加一个重复元素的剪枝条件
if sum(path) + nums[i] > target or (i > 0 and nums[i] == nums[i - 1]): continue path.append(nums[i]) self.back_trace(nums[i + 1:], result, path, target) path.pop()

 7、括号生成

  括号要有效的前提是右括号要在左括号的右边,所以可以先选择左括号,再选择右括号,并且保证已经选择的左括号的个数是要大于或等于右括号的。"""给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。


例如,给出 n = 3,生成结果为:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

"""


class Solution:
    def generateParenthesis(self, n: int):
        result = []
        path = []
    
     # left 和 right用来记录左括号和右括号的数量 self.back_trace(n, result, path, left=0, right=0) return result def back_trace(self, n, result, path, left, right):
# 终止条件
if len(path) == 2 * n: result.append("".join(path))
return
     # 先选择左括号
if left < n:
       path.append("(") self.back_trace(n, result, path
, left + 1, right)
path.pop()
     # 选择右括号时保证右括号的数量小于左括号
if right < left:
path.append(")") self.back_trace(n, result, path
, left, right + 1)
path.pop()

8、电话号码的字母组合

  本道题是一个二维的遍历,先遍历数字列表,再遍历每个数字对应的字母列表

"""
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。



示例:

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

"""


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

def letterCombinations(self, digits: str):
result = []
path = []
self.back_trace(digits, result, path, len(digits))
return result

def back_trace(self, digits, result, path, length):
if len(path) == length:
result.append("".join(path))
return
for i in range(len(digits)):
for j in range(len(self.d[digits[i]])):
path.append(self.d[digits[i]][j])
self.back_trace(digits[i + 1:], result, path, length)
path.pop()

9、八皇后问题

  有一个 8x8 的棋盘,往里放 8 个棋子,每个棋子所在的行、列、对角线都不能有另一个棋子。找出有多少种排列?

  八皇后的核心问题是如何判断冲突的问题,在摆放棋子的时候是按行逐个排列的,所以行不会冲突,但是列、对角线(左上、右上两条对角线)会有可能冲突,所以需要把这三个已发生的状态存储下来,对新的皇后选择时要判断和已选的是否冲突,冲突则直接跳过。

class Solution:
    def __init__(self):
        self.result = 0
        self.columns = set()  # 存储已经选择过的列
        self.diag_left = set()  # 存储已经选择过的左上角的元素
        self.diag_right = set()  # 存储已经选择过的右上角的元素

    def is_conflict(self, row, col):
        if col in self.columns or row - col in self.diag_left or row + col in self.diag_right:
            return True
        return False

    def eightQueen(self, n):
        self.dfs(0, n)
        return self.result

    def dfs(self, row, n):
        # 终止条件
        if row == n:
            self.result += 1
            return

        for col in range(n):
            # 剪枝
            if self.is_conflict(row, col):
                continue

            # 执行选择,保存状态
            self.columns.add(col)
            self.diag_left.add(row - col)
            self.diag_right.add(row + col)
            # 回溯
            self.dfs(row + 1, n)
            # 撤销选择,删除状态
            self.columns.remove(col)
            self.diag_left.remove(row - col)
            self.diag_right.remove(row + col)

 10、二叉搜索树II

  给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。

  树的生成用递归实现非常容易,确定根节点,递归生成左子树和右子树,最后将左指针和右指针分别指向左子树和右子树即可。对于二叉搜索树有中序遍历是有序的,所以对于一个1到n的有序数组,可以用中序遍历来生成二叉搜索树。

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


class Solution:
    def generateTrees(self, n: int):
        res = self.dfs(1, n)
        return res

    def dfs(self, start, end):
        if start > end:
            return [None]
        all_trees = []

     # 取出所有可以作为根节点的值
for i in range(start, end + 1): lefts = self.dfs(start, i - 1) rights = self.dfs(i + 1, end)        
       # 将所有的左子树和右子树组合挂到根节点上
for left in lefts: for right in rights: root = TreeNode(i) root.left = left root.right = right all_trees.append(root) return all_trees

 

posted @ 2022-06-27 15:20  微笑sun  阅读(234)  评论(0编辑  收藏  举报