LeetCode回溯算法从零到一

文章目录

    • 什么是回溯算法(Backtracking)
    • LeetCode题目列表
    • 39. 组合总和(Combination Sum)
    • 40. 组合总和 II(Combination Sum II)
    • 46. 全排列(Permutations)
    • 47. 全排列 II(Permutations II)
    • 78. 子集(Subsets)
    • 90. 子集 II(Subsets II)

什么是回溯算法(Backtracking)

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。——LeetCode

回溯法本质上是一种穷举或者说暴力(brute force),与暴力的不同之处在于,是一种聪明的暴力方法,聪明在于“不能进则退回来”,这减少了遍历的次数。

另外,回溯法可能还通常与递归DFS(Depth First Search,深度优先搜索)等名词被人提及,实际上这些方法在思想上都是一致的。

对于算法学习,keep your hands dirty就是捷径,在你没有实际写代码之前,看的再多可能也不可能深刻认识到某些问题。所以多说无益,下面将通过LeetCode上的一系列相关题目来说明回溯算法的思想,题目列表如下:


LeetCode题目列表

39. 组合总和(Combination Sum)
40. 组合总和 II(Combination Sum II)
46. 全排列(Permutations)
47. 全排列 II(Permutations II)
78. 子集(Subsets)
90. 子集 II(Subsets II)


39. 组合总和(Combination Sum)

LeetCode回溯算法从零到一_第1张图片

class Solution(object):
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        res = []
        def dfs(cur_list, cur_target, index):
            for i in range(index, len(candidates)):
                num = candidates[i]
                if num == cur_target:
                    res.append(cur_list + [num])
                elif num < cur_target:
                    dfs(cur_list + [num], cur_target - num, i)
        dfs([], target, 0)
        return res

代码并不复杂,代码思路的详细讲解,大家可以移步到LeetCode相应题目下的题解一栏,我并不打算增加互联网的冗余,在这里我只会说一些自己觉得需要注意到的点。

  • 题目描述中需要注意的:candidates无重复元素candidates中数字可取无限次
  • 内部函数dfs()的使用。dfs()每一次递归调用,实际上就是一次选路的过程,整个过程和深度优先搜索类似,所以个人喜欢把该函数命名为dfs。这是一个通用的模版,你也可以不使用内部函数,但如果语言支持内部函数,那请尽量使用,这样代码封装性较好。
  • 函数dfs()index参数,index保证了题目中“解集不能包含重复的组合”这一要求。可以这样理解,对于可选的数字candidates,如果先选了后一个数就不能再选前一个数了。以示例1中的输入为例,第一次进入dfs()时,当前可选数字为[2, 3, 6, 7],假设现在遍历到数字3,下一层递归中index = 1(数字3的数组索引为1),此时就不会再选到3的前一个数2
    那如果我们先选了后一个数就不再选前一个数,不会漏了一些组合吗?不会的。任选了一个数,这个数之后的所有数,都是可选的,这个数和它之后的所有数的组合都是可以取到的。然而此时,如果你选了后一个数,再去和它的前一个数进行组合,显然就发生了重复。

40. 组合总和 II(Combination Sum II)

LeetCode回溯算法从零到一_第2张图片

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        res = []
        candidates.sort()
        def dfs(cur_nums, path, cur_target):
            left = 0
            while left < len(cur_nums):
                num = cur_nums[left]
                if num == cur_target:
                    res.append(path + [num])
                    return
                elif num < cur_target:
                    dfs(cur_nums[left + 1:], path + [num], cur_target - num)
                    left += 1
                    while left < len(cur_nums) and cur_nums[left] == cur_nums[left - 1]:
                        left += 1
                else:
                    break
        dfs(candidates, [], target)
        return res
  • 39题的进化版,与之前不同的是,candidates中含有重复的数字;candidates中每个数字只能使用一次
  • 这题中我们是怎么处理重复的组合的呢?首先,对candidates进行了排序,那么为什么要排序呢,因为candidates有重复,并且题目没说candidates是有序的,排序之后就可以把重复的数字聚到一起,而这是我们希望要的效果,可以方便我们之后的操作。
    然后,在dfs()中,这里的思路还是和39题一致,就是先选了后一个数就不能再选前一个数了(这里用了cur_nums来保存当前递归中,我们可以选择的候选数字,这和上一题的index作用是一样的,你也可以继续使用index来限制候选数字)。但仅这样做还是会重复,原因还是candidates有重复,对于这个问题,我们的做法就是,先把当前位置后移一位left += 1,然后再比较cur_nums[left]cur_nums[left - 1]排序把重复数字聚到了一起),如果相等就把left不断往后移。

46. 全排列(Permutations)

LeetCode回溯算法从零到一_第3张图片

class Solution(object):
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = []
        def dfs(cur_nums, path):
            if not cur_nums:
                res.append(path)
            for (i, num) in enumerate(cur_nums):
                dfs(cur_nums[:i] + cur_nums[i + 1:], path + [num])
        dfs(nums, [])
        return res
  • nums没有重复,本题中需要的是排列,对于排列而言,次序也是重要信息,而之前两题是组合,组合不用考虑次序。
  • 因为次序不同是不同的排列,所以本题不用像之前那样(先选了后一个数就不能再选前一个数了),在这里我们使用cur_nums来保存当前候选数字,每次递归时当前候选数字变为了cur_nums[:i] + cur_nums[i + 1:],这相当于把当前数字拿掉了。

47. 全排列 II(Permutations II)

LeetCode回溯算法从零到一_第4张图片

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = []
        nums.sort()
        def dfs(cur_nums, path):
            if not cur_nums:
                res.append(path)
            left = 0
            while left < len(cur_nums):
                dfs(cur_nums[:left] + cur_nums[left + 1:], path + [cur_nums[left]])
                left += 1
                while left < len(cur_nums) and cur_nums[left] == cur_nums[left - 1]:
                    left += 1
        dfs(nums, [])
        return res
  • 包含重复数字的序列
  • 与上一题基本相同,关键在于如何去除重复数字导致的重复排列,具体办法与40题一致,就是先排序,然后遍历的时候,对于被排序到一块的相同的数字,我们只处理第一个

78. 子集(Subsets)

LeetCode回溯算法从零到一_第5张图片

class Solution(object):
    def subsets(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = []
        def dfs(index, path):
            res.append(path)
            for i in range(index, len(nums)):
                dfs(i + 1, path + [nums[i]])
        dfs(0, [])
        return res
  • 这里要解的是子集,并且nums不含有重复数字
  • 如果上面的题你都能做出来,那这题实际上也一样,唯一的区别就是每次递归都要res.append(path),因为求得是子集,结果中不重复的保证靠的还是那句话“先选了后一个数就不能再选前一个数了”,具体到代码中,是通过index来达到这个要求的。

90. 子集 II(Subsets II)

LeetCode回溯算法从零到一_第6张图片

class Solution(object):
    def subsetsWithDup(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        res = []
        nums.sort()
        def dfs(cur_set, index):
            res.append(cur_set)
            left = index
            while left < len(nums):
                dfs(cur_set + [nums[left]], left + 1)
                left += 1
                while left < len(nums) and nums[left] == nums[left - 1]:
                    left += 1
        dfs([], 0)
        return res
  • 上一题的含重复数字版本,关键是在上一题的基础上处理重复数字的问题
  • 重复数字的的解决办法,和之前的题目也如出一辙,还是老规矩,先排序,然后只处理重复数字中的第一个

你可能感兴趣的:(python,LeetCode,回溯算法)