算法模板-回溯

Posted 周先森爱吃素

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法模板-回溯相关的知识,希望对你有一定的参考价值。

简介

回溯法常用于遍历列表所有子集,是 DFS 深度优先搜索一种,一般用于全排列,穷尽所有可能,遍历的过程实际上是一个决策树的遍历过程。时间复杂度一般 O ( N ! ) O(N!) O(N!),它不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都比较高。

解题模板

回溯算法的解题一般是递归实现,可以抽象如下。最核心的思路就是从选择列表里做一个选择,然后一直递归往下搜索答案,如果遇到路径不通(搜索完成),就返回来撤销这次选择。

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

回溯的题目我们可以理解为一棵树的搜索过程,一方面要考虑树往深度推广(下一层),另一方面也要考虑树往广度推广(这一层)。其实回溯算法关键在于:不合适就退回上一步,然后通过约束条件,,减少时间复杂度。这两个层面正是发生在深度和广度上的。

题目列表

下面是力扣主站题库里典型的回溯求解的题。

题解列表

39-组合总和

原题链接

这道题我们也可以用回溯的思路来求解所有可能的结果,先选定第i个元素,然后回溯后面位置的可能组合。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates or min(candidates) > target: return []
        candidates.sort()
        ans = []
        
        def backtrack(candidates, target, tmp=[]):
            if target == 0: ans.append(tmp)
            if target < 0: return
            for i in range(len(candidates)):
                if candidates[i] > target:
                    break
                backtrack(candidates[i:], target - candidates[i], tmp + [candidates[i]])
        
        backtrack(candidates, target)
        return ans

40-组合总和 II

原题链接

这道题和上一题类似,区别只是每个数字只能使用一次,因此递归的时候下标要向后一位。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates or min(candidates) > target: return []
        candidates.sort()
        ans = []
        
        def backtrack(i, tmp_sum, tmp=[]):
            if tmp_sum == target:
                ans.append(tmp)
                return 
            
            for j in range(i, len(candidates)):
                if tmp_sum + candidates[j] > target: break
                if j > i and candidates[j] == candidates[j-1]: continue
                backtrack(j+1, tmp_sum + candidates[j], tmp + [candidates[j]])

        
        backtrack(0, 0)
        return ans

46-全排列

原题链接

这道题要求生成全排列,组合是排列的子集,排列认为位置不同就不是同一个结果。

这道题和子集那题很像,需要注意写法的区别,子集那题我们不断后移起始位置,但是本题我们始终保证全部位置供选择。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        def backtrack(nums, tmp=[]):
            if not nums: 
                res.append(tmp)
                return 
            for i in range(len(nums)):
                backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
        
        backtrack(nums)
        return res

47-全排列 II

原题链接

这道题很上一题类似,我们需要做个去重。

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        def backtrack(nums, tmp=[]):
            if not nums and tmp not in res: 
                res.append(tmp)
                return 
            for i in range(len(nums)):
                backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
        
        backtrack(nums)
        return res

78-子集

原题链接

这题是要求数组的所有子集,是回溯题中非常经典的题,我们可以很容易地想到,以i位置为第一个元素加上后续元素的子集。

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        
        ans = []

        def backtrack(start, tmp=[]):
            ans.append(tmp)
            for i in range(start, len(nums)):
                backtrack(i+1, tmp + [nums[i]])
            
        backtrack(0, [])

        return ans

90-子集 II

原题链接

这题排序后去重就行。

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        ans = []
        nums.sort()
        def backtrack(start, tmp=[]):
            if tmp not in ans:
                ans.append(tmp)
            for i in range(start, len(nums)):
                backtrack(i+1, tmp + [nums[i]])
            
        backtrack(0, [])

        return ans

补充说明

本文介绍了生成全排列等问题常见的回溯算法,但是解题时回溯用的可能并不多,因为本质上它就是一种暴力算法。

以上是关于算法模板-回溯的主要内容,如果未能解决你的问题,请参考以下文章

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

216. 组合总和 III-----回溯篇4

LeetCode216. 组合总和 III

回溯算法:组合总和

[回溯算法]leetcode40. 组合总和 II(c实现)