- Subsets
Given a set of distinct integers, return all possible subsets.
Example
Example 1:
Input: [0]
Output:
[
[],
[0]
]
Example 2:
Input: [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
Challenge
Can you do it in both recursively and iteratively?
Notice
Elements in a subset must be in non-descending order.
The solution set must not contain duplicate subsets.
SubSet 这题有4种解法,其中DFS两种,BFS一种,位运算一种。
解法1:DFS组合类模板。对应每个元素都有选或不选两种可能。
输入为[1,2,3]的递归树如下图。注意,最下面一行的每个节点均为一个可行解。

class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
int n = nums.size();
if (n == 0) return {{}};
vector<vector<int>> result;
vector<int> sol;
sort(nums.begin(), nums.end());
helper(nums, 0, result, sol);
return result;
}
private:
void helper(vector<int> & nums, int pos, vector<vector<int>> &result, vector<int> & sol) {
if (pos == nums.size()) {
result.push_back(sol);
return;
}
sol.push_back(nums[pos]);
helper(nums, pos + 1, result, sol);
sol.pop_back();
helper(nums, pos + 1, result, sol);
return;
}
};
解法2:
DFS 通用回溯解法。注意!上面那个DFS没有for loop,这里的DFS有for loop!
注意:
- 是每次进入helper()的sol都是一个解,所以不用等到index=nums.size()再存到results里面。
- vector < vector < int> > results;
results的值会是[]。如果想让它变成[[]],要用results.push_back(vector()); - 输入为[1,2,3]的递归树如下图。注意:其中每个节点均为一个可行解。
由此看来,解法2只调用了DFS8次,而解法1调用了DFS15次。

class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int> > results;
vector<int> sol;
if (nums.empty()) {
results.push_back(vector<int>());
return results;
}
sort(nums.begin(), nums.end());
helper(nums, 0, sol, results);
return results;
}
void helper(vector<int> &nums, int index, vector<int>&sol, vector<vector<int>> &results) {
results.push_back(sol);
for (int i=index; i<nums.size(); ++i) {
sol.push_back(nums[i]);
helper(nums, i+1, sol, results); //切记这里是i+1,不是index+1
sol.pop_back();
}
}
};
个人的一个总结:
1. 如果有重复元素,subsets和permutation都需要先排序。
2. 如果没有重复元素, permuation不需要先排序。subsets如果输出结果的每个subset不需要按字典序排序(也就是说(3,0,4)而不是(0,3,4)可以接受)的话,也不需要sort。否则如果输出结果的每个subset需要按字典序排序,那subsets这题就需要先排序。
二刷:
class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
* we will sort your return value in output
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> res;
int numsSize = nums.size();
if (numsSize == 0) return {{}};
sort(nums.begin(), nums.end());
vector<int> sol;
dfs(nums, 0, sol, res);
return res;
}
private:
void dfs(vector<int> &nums, int startIndex, vector<int> &sol, vector<vector<int>> &res) {
res.push_back(sol);
int numsSize = nums.size();
for (int i = startIndex; i < numsSize; i++) {
sol.push_back(nums[i]);
dfs(nums, i + 1, sol, res);
sol.pop_back();
}
}
};
Gemini的关于解法1和解法2的解答:
两个解法都是回溯算法(Backtracking)!在算法界,这两种写法通常被称为回溯的两种标准流派。只要代码中包含了回溯算法最核心的三个步骤:做选择(push_back)、向下递归(helper)、撤销选择(pop_back),它就属于回溯算法。它们只是在对“如何回溯”的决策树建模上产生了分歧:
解法 1:状态回溯(二叉决策树)回溯特征:它在对单个元素的状态进行回溯。
逻辑:我把 nums[pos] 放进背包(做选择),去探索后续可能性;回来后,我必须把它拿出来(撤销选择),再在不放它的情况下探索后续可能性。
形象比喻:抛硬币。每次面对一个数,你要么抛出正面(选),要么抛出反面(不选)。
解法 2:路径回溯(多叉组合树)回溯特征:它在对子集延伸的路径进行回溯。
逻辑:在这一层,我尝试把 nums[i] 作为子集的下一个元素(做选择),深入向下探索;回来后,我把它拿出来(撤销选择),以便腾出位置在下一次循环里尝试把 nums[i+1] 作为下一个元素。
形象比喻:挑衣服。你的衣柜里有几件衣服,你先试穿 A 看看效果(递归),脱下来(回溯),再试穿 B 看看效果。为什么大家更习惯把“解法 2”叫做经典回溯?虽然两者都是回溯,但在实际刷题和面试中,解法 2 的多叉树模板(带 for 循环)使用得更广泛。
原因有以下两点:更方便剪枝(去重):如果题目变成 Subsets II(数组里有重复元素,但结果不能重复)。解法 2 只需要在 for 循环里加一行:if (i > index && nums[i] == nums[i-1]) continue; 就能完美去重。解法 1 如果想去重,由于它没有 for 循环作为同层横向对比的锚点,去重逻辑会变得非常绕且难以理解。
天然契合“组合/排列”问题:回溯法最常用于解决排列(Permutations)和组合(Combinations)问题。解法 2 的 for 循环模板可以直接套用到几乎所有回溯题目中(如 N 皇后、电话号码的字母组合等),通用性极强。
解法1和解法2的区别:
- 根本区别:对“子集”构建方式的视角不同
解法 1:每个元素的“选与不选”(二叉树视角)视角:站在元素的角度。每个元素只有两种命运:要么被放进当前子集,要么不放进。递归树:一棵满二叉树。树的第 (i) 层决定第 (i) 个元素。收集时机:只有走到叶子节点(pos == nums.size()),把所有元素都看了一遍后,才收集一次结果。
解法 2:每一个子集的“长度延伸”(多叉树/组合树视角)视角:站在子集的角度。当前子集需要从剩下的元素里挑一个追加进来,直到没有元素可挑。递归树:一棵不规则的多叉树。收集时机:每走到一个新节点(无论是根节点还是中间节点),当前的 sol 本身就是一个合法子集,直接收集(results.push_back(sol))。 - 为什么解法 2 多了个 for 循环?因为解法 2 的多叉树在当前层有多个并列的候选人。以 nums = [1, 2, 3] 举例:如果当前子集是 [1],接下来你可以选择:加 2 (\rightarrow ) 变成 [1, 2]加 3 (\rightarrow ) 变成 [1, 3]这个 for (int i = index; i < nums.size(); ++i) 就是用来横向遍历当前所有可供选择的元素。每执行一次循环体,就代表探索一条新的子集延伸分支。
- 解法 2 为什么在 pop_back() 之后不再执行 helper?这是初学者最容易产生困惑的地方。我们把解法 2 的 for 循环拆开来看:cppfor (int i = index; i < nums.size(); ++i) {
sol.push_back(nums[i]); // 1. 做选择(比如选了nums[i])
helper(nums, i + 1, sol, results); // 2. 进入下一层递归,去穷举包含nums[i]的所有子集
sol.pop_back(); // 3. 撤销选择(把nums[i]弹出来)
}
Use code with caution:
原因:横向的 for 循环已经自动承担了“不选”的职责在 pop_back() 之后,紧接着会触发 ++i 进入下一次 for 循环。当 i 变成 i + 1,再次执行 sol.push_back(nums[i+1]) 时,你会发现,刚刚被 pop_back() 弹出的 nums[i] 并没有被放回去。这在实质上就等于**“不选 nums[i],而是直接去选 nums[i+1]”**。所以,解法 2 的“选与不选”是通过循环的推进实现的:helper 之前:选当前 nums[i],并深入向下。pop_back() 之后 + 进入下一次循环:不选刚才的 nums[i],转而去选下一个元素。因此,它不需要像解法 1 那样显式写两遍 helper。如果再写一遍 helper,就会导致重复搜索和逻辑混乱。
总结:两者的时空对比维度解法 1(选/不选)解法 2(for 循环延伸)树的形状严格的二叉树结构不固定 的多叉树递归调用次数刚好 (2^{n+1}-1) 次刚好 (2^{n}) 次(对应所有子集数量)结果收集位置只能在叶子节点(前序位置判断退出时)在每个节点的刚进入时(前序位置,无条件收集)
解法3:BFS解法:
用 BFS 来解决该问题时,层级关系如下:
第一层: []
第二层: [1] [2] [3]
第三层: [1, 2] [1, 3], [2, 3]
第四层: [1, 2, 3]
每一层的节点都是上一层的节点拓展而来。
注意:
- BFS的方法不需要sort。
- 一开始queue里要push空集{}。
代码如下:
class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> vv;
int n = nums.size();
queue<vector<int>> q;
q.push({}); //push {}
while(!q.empty()) {
vector<int> v = q.front();
q.pop();
vv.push_back(v);
for (int i = 0; i < n; ++i) {
if (v.size() == 0 || v.back() < nums[i]) {
vector<int> v2 = v;
v2.push_back(nums[i]);
q.push(v2);
}
}
}
return vv;
}
};
二刷
class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> vv;
int n = nums.size();
queue<vector<int>> q;
q.push({}); //push {}
while(!q.empty()) {
vector<int> v = q.front();
q.pop();
vv.push_back(v);
for (int i = 0; i < n; ++i) {
if (v.size() == 0 || v.back() < nums[i]) {
v.push_back(nums[i]);
q.push(v);
v.pop_back();
}
}
}
return vv;
}
};
解法4:位运算
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int> > solution;
vector<int> singleSol;
int numSize=nums.size();
int totalNum=pow(2,numSize)-1;
solution.push_back(singleSol);
for (int i=1; i<=totalNum; ++i) {
singleSol.clear();
for(int j=0; j<numSize; ++j) {
int bitNum = (i>>j)&0x1;
if (bitNum==1) {
singleSol.push_back(nums[j]);
}
sort(singleSol.begin(), singleSol.end());
}
solution.push_back(singleSol);
}
return solution;
}
效率可能还要改进一下。
另外2种下次写。
另外分析一下算法复杂度。关于DFS的时间复杂度是O(答案个数×构造每个答案时间)。所以以上DFS的时间复杂度都是O(n*2^n)。
===============================
18. Subsets II
Given a collection of integers that might contain duplicates, nums, return all possible subsets (the power set).
Example
Example 1:
Input: [0]
Output:
[
[],
[0]
]
Example 2:
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
Challenge
Can you do it in both recursively and iteratively?
Notice
Each element in a subset must be in non-descending order.
The ordering between two subsets is free.
The solution set must not contain duplicate subsets.
SubSet II 和SubSet类似,但是要注意去重。
以通用回溯解法为例:
vector<vector<int>> subsetsWithDup(vector<int> &nums) {
vector<vector<int>> results;
vector<int> sol;
if (nums.empty()) {
results.push_back(vector<int>());
return results;
}
sort(nums.begin(), nums.end());
helper(nums, 0, sol, results);
return results;
}
void helper(vector<int>&nums, int index, vector<int>&sol, vector<vector<int>>&results) {
results.push_back(sol);
for (int i=index; i<nums.size(); ++i) {
if ((i!=index) && (nums[i-1]==nums[i])) //去重
continue;
sol.push_back(nums[i]);
helper(nums, i+1, sol, results);
sol.pop_back();
}
}
为啥这个可以去重呢? 以[1,2,2]为例,

if ((i!=index) && (nums[i-1]==nums[i])) //去重
continue;
参考上面的代码,
第2行,取到[2]之后,就不会取第2个2了。
第3行,取到[1,2]之后,不会再取下一个[1,2]。同时它继续调用helper(,index+1,) 这里index+1=2,所以进helper()之后,index=2, i=2==index(第一次),所以第4行还是会有[1,2,2]。
同样,回溯到[2]之后会继续调用helper(,index+1,)。这里index+1=2。所以第3行还是会有[2,2],因为i==index。
网上的解答:
凡是i= index的时候,即便和前一个数相等,也是sol的第一次取到,也是最深的搜索结果一定要获得。 如[1,2,2]中sol=[1,2]开始搜索, index将指向第二个2开始搜索(index= 2),但由于开始i==index, 仍然需要取以获得[1,2,2],只因为和前一个数相等就continue会漏掉[1,2,2]。如果i > index, 那么这样的sol必然已经在之前取得过了并且被搜索过, 就是重复的结果了,同样对[1,2,2]计算,如sol=[1]的情况下去搜索, index= 1,即从第一个2组成的[1,2]进行搜索, 当完成并回溯后,下一个循环中,此时nums[2] = nums[1]且i = 2>index= 1, 同样的[1,2]已经在之前的回溯中都获得了全部的搜索结果,所以要continue.
i != index 实际上上保证递归到下一层的时候如果有重复数,还能被选入。比如[1,2,2],如果得到[1,2],dfs下一层应该是[1,2,2], 如果把i != index单纯换成i >0 的话,这里的第二个2就会被nums[i] == nums[i -1] 跳过而不加入subset,最后结果只有[1,2],没有[1,2,2]。
举例:
[1,2,2,2,2]
第一次 i=0,index=0; i=index; 1 入sol;
第二次 i=1,index=1;i=index;2 入sol;
第三次 i=2, index=2;nums[3]==nums[2]&&i=index, 跳过(continue, 所以index 并不增加);
第四次 i=3,index=2; nums[4]==nums[3]&&i>index, 跳过(continue);
解法2:Subset II也可以用BFS。我参考了Subset的解法,不过这里我用了sort和multimap。
代码如下:
class Solution {
public:
/**
* @param nums: A set of numbers.
* @return: A list of lists. All valid subsets.
*/
vector<vector<int>> subsetsWithDup(vector<int> &nums) {
vector<vector<int>> vv;
int n = nums.size();
sort(nums.begin(), nums.end());
unordered_map<int, int> um; //number, count
for (auto i : nums) {
if (um.find(i) == um.end()) {
um[i] = 1;
} else {
um[i]++;
}
}
queue<vector<int>> q;
q.push({}); //push {}
while(!q.empty()) {
vector<int> v = q.front();
q.pop();
vv.push_back(v);
unordered_map<int, int> um_v;
for (auto i : v) {
if (um_v.find(i) == um_v.end()) {
um_v[i] = 1;
} else {
um_v[i]++;
}
}
for (int i = 0; i < n; ++i) {
if (v.size() == 0 ||
v.back() < nums[i] ||
((v.back() == nums[i]) && (um_v[nums[i]] < um[nums[i]]))) {
vector<int> v2 = v;
v2.push_back(nums[i]);
if (q.back() != v2) {
q.push(v2);
}
}
}
}
return vv;
}
};
代码同步在
https://github.com/luqian2017/Algorithm
2452



被折叠的 条评论
为什么被折叠?



