回朔算法

214 阅读4分钟

1. 算法背景

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

2. 解决问题的一般步骤

  1. 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
  2. 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
  3. 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

3. 回溯算法框架

实际上是穷举的过程,代码的递归形式中主要体现为做选择和撤销选择
决策出口,即何时觉得可以输出到结果中了。
如何做出选择,从待选列表nums中去选择元素了,可能会需要nums排序,或在有重复数字的时候进行剪枝。

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

4. 适合算法的例子

4.1 全排列

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

def permute(nums):
    if len(nums) == 0:
        return []
    res = []

    def _backtrace(arr: list, pre_list: list):
        # 递归出口,满足条件 加入数组
        if len(arr) == 0:
            # 这里需要进行深拷贝 不然会记录会根据pre_list改变 最后导致全部为空列表
            res.append(pre_list[:])
            return
        for i in range(len(arr)):
            # 1. 做选择-> 开始进行一些根据题意的判断等
            pre_list.append(arr[i])
            left_list = arr[:]
            left_list.remove(arr[i])
            # 2. 递归做出剩余选择->需要判断左侧新的数组从哪个位置开始
            _backtrace(left_list, pre_list)
            # 3. 撤销之前使用过的选择 -> 把当前值移出路径,才能进入下一个值的路径
            pre_list.pop()

    _backtrace(nums, [])
    return res


if __name__ == '__main__':
    print(permute([1, 2, 3]))
# 输入 [1,2,3] 输出 [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

4.2 全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列

def permute(nums):
    if len(nums) == 0:
        return []
    res = []

    def _backtrace(arr: list, pre_list: list):
        if len(arr) == 0:
            res.append(pre_list[:])
            return
        for i in range(len(arr)):
            # # 当前元素和上一个元素一样,不用重复加入了,进行剪枝
            if i > 0 and arr[i] == arr[i - 1]:
                continue
            pre_list.append(arr[i])
            left_list = arr[:]
            left_list.remove(arr[i])
            _backtrace(left_list, pre_list)
            pre_list.pop()

    _backtrace(nums, [])
    return res


if __name__ == '__main__':
    print(permute([1, 2, 2]))
# 输出 [[1, 2, 2], [2, 1, 2], [2, 2, 1]]

4.3 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集,解集不能包含重复的子集。
决策出口:这道题是只要进入回溯函数,就输出到结果中,因为没有要求当前的路径元素个数的限制。2)如何做出选择:按顺序从nums中加入到pre_list中即可

def subsets(nums: list):
    if len(nums) == 0:
        return []
    nums.sort()
    res = []

    def _backtrace(arr: list, pre_list: list):
        res.append(pre_list[:])
        for i in range(len(arr)):
            # if i > 0 and arr[i] == arr[i - 1]:
            #     continue
            pre_list.append(arr[i])
            _backtrace(arr[i + 1:], pre_list)
            pre_list.pop()

    _backtrace(nums, [])
    return res


if __name__ == '__main__':
    print(subsets([1, 2, 3]))
    # 输出:[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

4.4 子集 II

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

def subsets(nums: list):
    if len(nums) == 0:
        return []
    nums.sort()
    res = []

    def _backtrace(arr: list, pre_list: list):
        res.append(pre_list[:])
        for i in range(len(arr)):
            if i > 0 and arr[i] == arr[i - 1]:
                continue
            pre_list.append(arr[i])
            _backtrace(arr[i + 1:], pre_list)
            pre_list.pop()

    _backtrace(nums, [])
    return res


if __name__ == '__main__':
    print(subsets([1, 2, 2]))

4.5 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。

1)决策出口:当pre_list路径中所有数之和等于target,输出到res中。
2)做出选择:由于每个数字可以使用多次,所以nums就是原数组。此外,这里可以使用剪枝来降低复杂度——对原数组排序,若从待选列表中加入当前数之后的总和大于target,那么该数之后的元素都不需要考虑了。

def combination_sum(nums: list, target: int):
    if len(nums) == 0:
        return []
    nums.sort()
    res = []

    def _backtrace(arr: list, pre_list: list):
        if sum(pre_list) == target:
            res.append(pre_list[:])
            return
        for i in range(len(arr)):
            left_num = target - arr[i] - sum(pre_list)
            # 剪枝
            if left_num < 0:
                break
            pre_list.append(arr[i])
            # arr更新为该数及该数之后
            _backtrace(arr[i:], pre_list)
            # 把当前值移出路径,才能进入下一个值的路径
            pre_list.pop()

    _backtrace(nums, [])
    return res


if __name__ == '__main__':
    print(combination_sum([2, 3, 6, 7], 7))
# 输出 :[[2, 2, 3], [7]]

4.6 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。