一.题目: 子集
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