python - leetcode - 78. 子集&90. 子集II【经典题解 - 回溯算法】

一.题目: 子集

78. 子集
描述:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10 nums 中的所有元素 互不相同

二. 解题思路

回溯法是一种算法思想,而递归是一种编程方法,回溯法可以用递归来实现。

回溯法的整体思路是:搜索每一条路,每次回溯是对具体的一条路径而言的。对当前搜索路径下的的未探索区域进行搜索,则可能有两种情况:

  • 当前未搜索区域满足结束条件,则保存当前路径并退出当前搜索;
  • 当前未搜索区域需要继续搜索,则遍历当前所有可能的选择:如果该选择符合要求,则把当前选择加入当前的搜索路径中,并继续搜索新的未探索区域。

上面说的未搜索区域是指搜索某条路径时的未搜索区域,并不是全局的未搜索区域。

回溯法搜所有可行解的模板一般是这样的:

res = []
path = []

def backtrack(未探索区域, res, path):
    if path 满足条件:
        res.add(path) # 深度拷贝
        # return  # 如果不用继续搜索需要 return
    for 选择 in 未探索区域当前可能的选择:
        if 当前选择符合要求:
            path.add(当前选择)
            backtrack(新的未探索区域, res, path)
            path.pop()

backtrack 的含义是: 未探索区域中到达结束条件的所有可能路径,path 变量是保存的是一条路径,res 变量保存的是所有搜索到的路径。所以当「未探索区域满足结束条件」时,需要把 path 放到结果 res 中。
path.pop() 是啥意思呢?
它是编程实现上的一个要求,即我们从始至终只用了一个变量 path,所以当对 path 增加一个选择并 backtrack 之后,需要清除当前的选择,防止影响其他路径的搜索。

正规写法

对于 78. 子集 而言,找出没有重复数字的数组所有子集,按照模板,我们的思路应该是这样的:

  • 未探索区域:剩余的未搜索的数组 nums[index: N - 1] ;
  • 每个 path 是否满足题目的条件: 任何一个 path 都是子集,都满足条件,都要放到 res 中 ;
  • 当前 path 满足条件时,是否继续搜索:是的,找到 nums[0:index-1] 中的子集之后, nums[index] 添加到老的
    path 中会形成新的子集。
  • 未探索区域当前可能的选择:每次选择可以选取 s 的 1 个字符,即 nums[index] ;
  • 当前选择符合要求:任何 nums[index] 都是符合要求的,直接放到 path 中;
  • 新的未探索区域:nums 在 index 之后的剩余字符串, nums[index + 1 : N - 1] 。
class Solution(object):
    def subsets(self, nums):
        res, path = [], []
        self.dfs(nums, 0, res, path)
        return res
    
    def dfs(self, nums, index, res, path):
        res.append(copy.deepcopy(path))
        for i in range(index, len(nums)):
            path.append(nums[i])
            self.dfs(nums, i + 1, res, path)
            path.pop()

简化写法

上面是正规的回溯写法,如果想偷懒,可以每次在搜索的时候都新建一个 path 变量,而不是复用全局的 path。那么代码可以更精简。

如下面所写,每次寻找新的子集的时候,都新建了一个 path,因为 path + [nums[i]] 返回的是一个新的列表,放在函数的参数里面,每次传过来的都是新的,所以 res.append(path) 时不用深度拷贝。

class Solution(object):
    def subsets(self, nums):
        res = []
        self.dfs(nums, 0, res, [])
        return res
    
    def dfs(self, nums, index, res, path):
        res.append(path)
        for i in xrange(index, len(nums)):
            self.dfs(nums, i + 1, res, path + [nums[i]])

少传参数写法

class Solution(object):
    def subsets(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        # 写法一
        res = []
        n = len(nums)
        def helper(i, tmp):
            res.append(tmp)
            for j in range(i, n):
                helper(j + 1,tmp + [nums[j]] )
        helper(0, [])
        return res 

		# 写法二
        res, path = [], []
        def dfs(index, res, path):
            res.append(copy.deepcopy(path))
            for i in range(index, len(nums)):
                path.append(nums[i])
                dfs(i + 1, res, path)
                path.pop()
        dfs(0, res, path)
        return res

三. 其他解法

思路一、库函数

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

思路二:迭代,一次遍历(模拟),不需要回溯

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        for i in range(len(nums)):
            new_subsets = [subset + [nums[i]] for subset in res]
            res = new_subsets + res
        return res

简化写法

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        for num in nums:
            res = res + [[num] + i for i in res]
        return res

思路三:递归(回溯算法)

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        n = len(nums)
       
        def helper(i, tmp):
            res.append(tmp)
            for j in range(i, n):
                helper(j + 1,tmp + [nums[j]] )
        helper(0, [])
        return res 

四. 题目变换: 子集II

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

正规写法

如果你能理解上面的回溯法,那么包含重复元素的数组的子集,只不过一个小的改进。

比如说求 nums = [1,2,2] 的子集,那么对于子集 [1,2] 是选择了第一个 2,那么就不能再选第二个 2 来构成 [1,2] 了。所以,此时的改动点,就是先排序,每个元素 nums[i] 添加到 path 之前,判断一下 nums[i] 是否等于 nums[i - 1] ,如果相等就不添加到 path 中。

class Solution(object):
    def subsetsWithDup(self, nums):
        res, path = [], []
        nums.sort()
        self.dfs(nums, 0, res, path)
        return res
        
    def dfs(self, nums, index, res, path):
        res.append(copy.deepcopy(path))
        for i in range(index, len(nums)):
            if i > index and nums[i] == nums[i - 1]:
                continue
            path.append(nums[i])
            self.dfs(nums, i + 1, res, path)
            path.pop()

少传参数写法

class Solution(object):
    def subsetsWithDup(self, nums):
        nums.sort()
        res = []
        def back_tracking(start, temp):
            res.append(temp[:])
            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i-1]:
                    continue
                temp.append(nums[i])
                back_tracking(i+1, temp)
                temp.pop()
        back_tracking(0, [])
        return res

简化写法

class Solution(object):
    def subsetsWithDup(self, nums):
        res = []
        nums.sort()
        self.dfs(nums, 0, res, [])
        return res
        
    def dfs(self, nums, index, res, path):
        if path not in res:
            res.append(path)
        for i in range(index, len(nums)):
            if i > index and nums[i] == nums[i - 1]:
                continue
            self.dfs(nums, i + 1, res, path + [nums[i]])

少传参数写法

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        nums.sort()
        n = len(nums)
        def helper(i, tmp):
            if tmp not in res:
                res.append(tmp)
            for j in range(i, n):
                if i > j and nums[i] == nums[i - 1]:
                    continue
                helper(j + 1, tmp + [nums[j]])
        helper(0, [])
        return res

其他解法

思路一:一次遍历(模拟),不需要回溯

class Solution(object):
    def subsetsWithDup(self, nums):
        res = [[]]
        nums.sort()
        for i in range(len(nums)):
            if i >= 1 and nums[i] == nums[i-1]:
                new_subsets = [subset + [nums[i]] for subset in new_subsets]
            else:
                new_subsets = [subset + [nums[i]] for subset in res]
            res = new_subsets + res
        return res

简化写法

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        res=[[]]
        for n in nums:
            res+=[i+[n] for i in res if i+[n] not in res]
        return res

思路二:统计每个数字的频次

不用去重也不用排序

# 刚开始我们只有空集一个答案,循环所有可能的数字,
# 每次循环我们对当前答案的每一种情况考虑加入从1到上限次该数字并更新答案即可
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        dic = {
    
    }
        for i in nums:
            dic[i] = dic.get(i, 0) + 1
        res = [[]]
        for i, v in dic.items():
            temp = res.copy()
            for j in res:
                temp.extend(j+[i]*(k+1) for k in range(v))
            res = temp
        return res

猜你喜欢

转载自blog.csdn.net/qq_43030934/article/details/131642480