给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
用位技巧
记原序列中元素的总数为 n。原序列中的每个数字的状态可能有两种,即「在子集中」和「不在子集中」。我们用 1 表示「在子集中」, 0表示不在子集中,那么每一个子集可以对应一个长度为 n 的 二进制序列,第 i 位表示 对应元素是否在子集中。
如 2 3 4 n=3
序列 子集 序列对应的二进制数
0 [] 000
1 [4] 001
2 [3] 010
3 [3,4] 011
4 [2] 100
5 [2,4] 101
6 [2,3] 110
7 [2,3,4] 111
可以发现序列对应的二进制数正好从0到2^n-1 。我们可以枚举这些序列,按照当前数序列对应的二进制数在原集合当中取数。
下面注意几个位技巧 a << n 表示将a这个数对应的二进制为左移n位,在数学意义上就是乘以n个2,每移动1位就是乘以1个2。因此2^n-1就是1<<n - 1
另外还有与操作的一个技巧,就是当一个数a的二级制序列只有第 i 位是1其他都是0的时候,用a和任意一个数b做与操作,如果结果不为0说明b在第 i 位也是1,如果结果为0说明b在第 i 位上为 0。因此用a可以去检验任何一个数b的二进制的第i位是否为1。因此我们在代码里面用 1 << pos pos=【0-n)去逐个检验长度为n的二进制序列mask的第pos位是否为1,如果为1, 则把原始数组的第pos个加到结果中去
由于有2^n个序列,每个序列都要遍历n个位置去检验是否该把这个位置的元素加到结果中,则时间复杂度O(2的n次方 * n) 。 空间复杂度O(n)因为这是临时数组sub的大小
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
output = []
for mask in range(0, 1 << n):
sub = []
for pos in range(n):
if mask & (1 << pos):
sub.append(nums[pos])
output.append(sub)
return output
1 全局变量: 保存结果
2 参数设计: 递归函数的参数,是将上一次操作的合法状态当作下一次操作的初始位置。这里的参数,我理解为两种参数:状态变量和条件变量。(1)状态变量(state)就是最后结果(result)要保存的值;(2)条件变量就是决定搜索是否完毕或者合法的值。
3 完成条件: 完成条件是决定 状态变量和条件变量 在取什么值时可以判定整个搜索流程结束。搜索流程结束有两种含义: 搜索成功并保存结果 和 搜索失败并返回上一次状态。
4 递归过程: 传递当前状态给下一次递归进行搜索。
res = [] # 定义全局变量保存最终结果
state = [] # 定义状态变量保存当前状态
p,q,r # 定义条件变量(一般条件变量就是题目直接给的参数)
def back(状态,条件1,条件2,……):
if # 不满足合法条件(可以说是剪枝,可选)
return
if # 状态满足最终要求(可选)
res.append(state) # 加入结果,必须有
return
# 主要递归过程,一般是带有 循环体 或者 条件体
for # 满足执行条件
if # 满足执行条件
back(状态,条件1,条件2,……)
back(状态,条件1,条件2,……)
return res
这种需要告知具体路径的,就不能用动态规划了,只能回溯
1, 以当前位置为源流往下摸排所有可以跳到的位置
2, 最终递归返回源流位置
3, 然后再以下面一个位置作为源流位置,重复上述操作
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
search = []
self._dfs(nums, 0, search, result)
return result
def _dfs(self, nums, index, search, result):
# 求所有子集的问题,不需要完成条件,因为任何一个状态都是一个子集
result.append(search.copy())
for i in range(index, len(nums)):
# 以当前位置为源流
search.append(nums[i])
# 往下摸排所有可以跳到的位置
self._dfs(nums, i + 1, search, result)
# 最终递归返回源流位置,然后再以下面一个位置作为源流位置,重复上述操作
search.pop()
对比 leetcode 90 子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
这个例子其实每个元素依然是只能用一次,只有一个不同就是有重复元素
比如[2,2,3] ,如果按78这题的做法,可能出现[2,3], [2,3],两个2并不同,但是其实这个组合重复了。因此要加一个限制
class Solution:
def subsetsWithDup(self, nums):
res = [] # 定义全局变量保存最终结果
state = [] # 定义状态变量保存当前状态
def back(state,q):
# 求所有子集的问题,不需要完成条件,因为任何一个状态都是一个子集
res.append(state.copy())
# 主要递归过程,一般是带有 循环体 或者 条件体
for i in range(q,len(nums)):
# 由于原始数组元素有重复,需要加这个条件避免重复组合
if (i > q) and (nums[i] == nums[i-1]):
continue
state = state + [nums[i]]
back(state,i+1)
state.pop()
nums.sort()
back(state,0)
return res
最后对比一道剑指offer 38
输入一个字符串,打印出该字符串中字符的所有排列, 字符串里面有重复元素。你可以以任意顺序返回这个字符串数组,但不能出现重复排列。
前面几道题,是组合问题,也就是所谓一个组合就是用些什么元素,元素之间的顺序是无所谓的,每种组合给出一种顺序的结果就可以了。所以在摸牌的时候,都是以下一个元素为源流往后摸牌。而这道题要求的是组合,也就是每种顺序都是一个结果。主要看下摸牌的方法,是把除了当前元素以外的其他元素全部加入下一步摸牌。
def permutation(s: str):
res = [] # 定义全局变量保存最终结果
state = "" # 定义状态变量保存当前状态
def back(state, s):
# 全排列问题需要用上全部元素,因此有完成条件
if len(s) == 0: # 状态满足最终要求
res.append(state) # 加入结果
return
for i in range(len(s)):
# 这个地方是同理的,由于原始字符串存在重复元素,因此
# 如果不加这个条件,会出现重复的排列
if (i > 0) and (s[i] == s[i-1]):
continue
state = state + s[i]
back(state, s[:i]+s[i+1:])
state = state[:-1]
s = list(s)
s.sort()
back(state, s)
return res
print(permutation('aac'))
['aac', 'aca', 'caa']