算法刷题自记录 | Leetcode90. 子集II,47. 全排列II,40. 组合总和II(递归+回溯+剪枝)

总结了几类遇到的递归+回溯题。其实总的思路都大差不差,主要是剪枝时需要挨个分析一下。

第一类(求子集)

Leetcode 90. 子集II

题目描述:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:

输入:nums = [0]
输出:[[],[0]]
 

提示:

1 <= nums.length <= 10
-10 <= nums[i] <= 10

思路

  本题总思路与DFS一致,只不过因为求的是子集而不是全排列,所以需要在DFS每深入一步时就将当前的tmp(子集)添加进result列表。这里的重点是剪枝条件,题目中的要求是“解集不能包含重复的子集”,以示例1为例的话,假设nums中的两个'2'分别为'2a'和'2b',即解集中不能同时出现[1, 2a, 2b]和[1, 2b, 2a]这样的子集。

  如果将子集的DFS过程想象为树结构的话,就相当于树的同一层级中不能出现重复的数字。这里偷个懒借用一下Leetcode上的高赞题解(代码随想录,附上链接:力扣)中画的图,以便更容易理解树结构。

算法刷题自记录 | Leetcode90. 子集II,47. 全排列II,40. 组合总和II(递归+回溯+剪枝)_第1张图片

   转变为代码思路的话,也就是同一层的for循环中,不能出现重复的数字。那么为了实现去重,需要先将nums进行排序,排序后,相同的数字必定会挨在一起,所以根据nums[i] == nums[i - 1]即可判断nums[i]是否已经出现过了。

Python3代码

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        result = []
        nums.sort()    # 进行剪枝的话,需要先对nums排序

        def dfs(begin, tmp):
            result.append(tmp)
            for i in range(begin, len(nums)):
                if i > begin and nums[i] == nums[i - 1]:  # 剪枝:把tmp想象成树结构的话,相当于不允许出现同层级中相同数字重复的情况。
                    continue
                dfs(i + 1, tmp + [nums[i]])

        dfs(0, [])
        return result

第二类(求全排列)

Leetcode 47. 全排列II

题目描述:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]
示例 2:

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

提示:

1 <= nums.length <= 8
-10 <= nums[i] <= 10

思路

  本题的解题思路和Leetcode 90. 子集II相同,差别只是在进行剪枝时,求子集的判断条件是 if i > begin and nums[i] == nums[i - 1],因为递归的时候下一个startIndex(begin)是i+1而不是0,所以直接 i > startIndex即可。而求全排列时的判断条件是 if i > 0 and nums[i] == nums[i-1] and visited[i-1] == 0。之所以全排列需要使用额外的visited数组判断是因为求全排列时每次要从0开始遍历,所以为了跳过已经入栈(path)的元素,就需要额外一个数组判断当前元素是否已经入栈。而 visited[i-1] == 0,则表示的是剪掉重复数字第二次出现的那条树枝,即剪掉第二个重复元素的递归。比如 nums = [1, 1, 2],进行第一次for循环时,遍历到的第一个1对应的 visited = [1, 0, 0],第二个1对应的 visited = [0, 1, 0],即 visited[i-1] = visited[0] == 0,进行剪枝。

Python3代码

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        result = []
        path = []
        n = len(nums)
        visited = [0] * n   # 为1则表示nums[i]已经访问过了
        nums.sort()         # 对原数组排序,保证相同的数字相邻,以便后面进行剪枝操作

        def backtrack(path):
            if len(path) == n:
                # if path not in result:        # 如果不进行剪枝,就需要在添加至result中时去除重复。这样时间会慢超过十倍
                result.append(path[:])
                return

            for i in range(n):
                if visited[i]:
                    continue


                # 以下判断,因为是求全排列,每次要从0开始遍历,为了跳过已入栈的元素,需要使用visited来判断是否已经入栈。
                # 如果是像求子集,可以不用visited数组来去重,因为递归的时候下一个startIndex是i+1而不是0,所以直接 i > startIndex即可。
                if i > 0 and nums[i] == nums[i-1] and visited[i-1] == 0:    # 剪枝:解决重复问题,即保证在添加第i个数时,重复数字只会被添加一次
                    continue                                                
                visited[i] = 1
                path.append(nums[i])
                backtrack(path)
                path.pop()
                visited[i] = 0      # path回溯了之后,visited[i]也需要进行回溯
        
        backtrack(path)
        return result

第三类(求组合)

Leetcode 39. 组合总和

题目描述:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:

输入: candidates = [2], target = 1
输出: []
 

提示:

1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都 互不相同
1 <= target <= 500

思路

  本题由于求的是组合总和,所以剪枝条件可以从总和的值上进行思考。因为 1 <= candidates[i] <= 200,所以当当前组合path总和大于target,即 target - sum(path) < 0 时,不管之后组合再加进什么数,都不可能使得 target - sum(path) == 0 ,所以这时就可以剪枝了。

Python3代码

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        path = []
        candidates.sort()

        def backtrack(begin, path, target):
            if not target:
                result.append(path[:])
                return
            
            for i in range(begin, len(candidates)):     # 避免path重复,即同时出现[2, 2, 3]和[2, 3, 2]这样的情况
                res = target - candidates[i]
                if res < 0:         # 剪枝:即如果当前path的元素和已经大于target,则进行剪枝
                    break
                path.append(candidates[i])
                backtrack(i, path, res)
                path.pop()

        backtrack(0, path, target)
        return result

Leetcode 40. 组合总和II

题目描述:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

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

注意:解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[[1,1,6],
[1,2,5],
[1,7],
[2,6]]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[[1,2,2],
[5]]
 

提示:

1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30

Python3代码

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        path = []
        candidates.sort()

        def backtrack(begin, path, target):
            if not target:
                result.append(path[:])
                return
            
            for i in range(begin, len(candidates)):
                res = target - candidates[i]
                if res < 0:
                    break
                if i > begin and candidates[i] == candidates[i - 1]:    # 剪枝
                    continue
                
                path.append(candidates[i])
                backtrack(i + 1, path, res)
                path.pop()
        
        backtrack(0, path, target)
        return result

你可能感兴趣的:(Leetcode刷题,算法,剪枝,leetcode,python)