78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
思路一,全集中的元素是否在子集中,全集中的每个元素选与不选,组成所有子集:
叶子节点做记录
递归终止条件是显示的:
if(idnex == len) {
res.add(new ArrayList<>(stack));
return;
}
-
1.求子集是全集的一部分:讨论全集的每一个元素是否在子集中,求所有子集。
-
转换为代码:每一个数选与不选。也可以理解为全集的每一个元素在不在子集中。
-
每一步:选不选全集中的数
-
叶子节点就是答案,为了得到叶子节点,可以使用深度优先搜索或广度优先搜索,在遍历到叶子节点时,将叶子节点的数字添加到结果集中。
- 1.1.深度优先遍历:
- 不断的向下深入,直到到达底部,不撞南墙不回头,再向上弹,再去不断向下深入,直到到达底部,遍历的路径像一根伸缩的绳子。
- 只用一个状态变量path记录叶子节点数的状态,向下深入一层:添加一个元素到状态变量path, 返回上一层,从状态变量中弹出一个元素。
- 1.2.广度优先遍历:
- 不同层之间的差异很大,遍历时,节点状态差异很大,比如:第三层尾到第四层头。所以需要每一个节点都创建一个状态节点,因为遍历时,一个状态变量不方便记录所有状态。
- 对比深度优先遍历,遍历时,不同节点状态差异很小。
-
2.为什么回溯算法效率高:
- 深度优先搜索有回退的过程,所以又叫回溯算法。可以用较少的状态变量去搜索所有可能的状态值。在状态空间较大的情况下,使用较少的变量完成搜索,强大搜索算法之一。
- 为什么能用较少状态变量?
- 因为深度搜索,一条道走到黑,然后在向上一层弹回,继续向下层搜索。状态节点之间的差异较小。方便用一个状态变量记录节点状态。
- 因为深度搜索,一条道走到黑,然后在向上一层弹回,继续向下层搜索。状态节点之间的差异较小。方便用一个状态变量记录节点状态。
-
3.体会回溯算法状态重置。
-
3.1先考虑不选择,后考虑选择
private void dfs(int[] nums, int index, int len,
Stack<Integer> stack, List<List<Integer>> res) {
if(idnex == len) {
res.add(new ArrayList<>(stack));
return;
}
// 不选,直接进入下一层
dfs(nums, idnex + 1, len, stack, res);
// 选了,进入下一层
stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
}
不选,直接进入下一层:
状态变量不变,不用改变和重置
dfs(nums, index + 1, len, stack, res);
选了nums[index],stack.add(nums[index]),;
进入下一层,dfs(nums, index + 1, len, stack, res);
返回上一层后:stack.pop(); 重置状态变量
stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
- 3.2 在考虑选择
private void dfs(int[] nums, int index, int len,
Stack<Integer> stack, List<List<Integer>> res) {
if(idnex == len) {
res.add(new ArrayList<>(stack));
return;
}
// 选了,进入下一层
stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
// 不选,直接进入下一层
dfs(nums, idnex + 1, len, stack, res);
}
- 代码
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len == 0) {
return res;
}
Stack<Integer> stack = new Stack<>();
dfs(nums, 0, len, stack, res);
return res;
}
private void dfs(int[] nums, int index, int len,
Stack<Integer> stack, List<List<Integer>> res) {
if (index == len) {
res.add(new ArrayList<>(stack));
return;
}
// 当前数可选,也可以不选
// 不选,直接进入下一层
dfs(nums, index + 1, len, stack, res);
// 选了有,进入下一层
stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
}
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len == 0) {
return res;
}
Stack<Integer> stack = new Stack<>();
dfs(nums, 0, len, stack, res);
return res;
}
private void dfs(int[] nums, int index, int len,
Stack<Integer> stack, List<List<Integer>> res) {
if (index == len) {
res.add(new ArrayList<>(stack));
return;
}
// 当前数可选,也可以不选
// 选了有,进入下一层
stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
// 不选,直接进入下一层
dfs(nums, index + 1, len, stack, res);
}
思路2:全排列思想,一位一位的去考虑,这些位置可以填哪些元素。
不是在叶子节点做记录,而是所有节点都记录下来
递归终止条件是隐式的:for循环完后,递归结束
private void find(int[] nums, int begin, List<Integer> pre) {
res.add(new ArrayList<>(pre)); // 没有显式递归终止
for (int i = begin; i< nums.length; i++) {
pre.add(nums[i]);
find(nums, i + 1, pre);
pre.remove(pre.size() - 1); // 组合问题,状态递归完成后要重置
}
}
-
1.遍历过程
-
第一层:
- 第一个位置可以填的元素是3个,1, 2, 3
-
第二层:
- 第一个位置填1的节点第二个位置可以填的元素是2个,2, 3 --> (1,2), (1,3)
- 第一个位置填2的节点,第二个位置可以填的元素是1个, 2 (2,3), 因为(2,1)=(1,2);
- 第一个位置填3的节点,第二个位置可以填的元素是0个。
-
第三层:
- 第一个位置填1,2的节点,可以填3。
-
按顺序考虑,填完之后不能回头,就不会重复。
-
和全排列问题的不同。
- 递归终止条件是隐式的:for循环完后,递归结束
private void find(int[] nums, int begin, List<Integer> pre) {
res.add(new ArrayList<>(pre)); // 没有显式递归终止
for (int i = begin; i< nums.length; i++) {
pre.add(nums[i]);
find(nums, i + 1, pre); // 下一层是i+1这个变量
pre.remove(pre.size() - 1); // 组合问题,状态递归完成后要重置
}
}
- 对比每一层的选择,递归终止条件是显式的,到达叶子节点递归就结束,记录叶子节点状态:
if(idnex == len) {
res.add(new ArrayList<>(stack));
return;
}
- 在回溯的过程中记录结点。
import java.util.ArrayList;
import java.util.List;
public class Solution {
private List<List<Integer>> res;
private void find(int[] nums, int begin, List<Integer> pre) {
// 没有显式的递归终止
res.add(new ArrayList<>(pre));// 注意:Java 的引用传递机制,这里要 new 一下
for (int i = begin; i < nums.length; i++) {
pre.add(nums[i]);
find(nums, i + 1, pre);
pre.remove(pre.size() - 1);// 组合问题,状态在递归完成后要重置
}
}
public List<List<Integer>> subsets(int[] nums) {
int len = nums.length;
res = new ArrayList<>();
if (len == 0) {
return res;
}
List<Integer> pre = new ArrayList<>();
find(nums, 0, pre);
return res;
}
}
- 在回溯的过程中记录深度。
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
int size = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (size == 0) {
return res;
}
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < size + 1; i++) {
dfs(nums, 0, i, stack, res);
}
return res;
}
private void dfs(int[] nums, int start, int depth, Stack<Integer> path, List<List<Integer>> res) {
if (depth == path.size()) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i + 1, depth, path, res);
path.pop();
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
Solution solution = new Solution();
List<List<Integer>> subsets = solution.subsets(nums);
System.out.println(subsets);
}
}
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/subsets/solution/hui-su-python-dai-ma-by-liweiwei1419/
状态变量,回溯函数的参数
结束条件
选择列表,与第一步紧密相关
判断是否需要剪枝
做出选择,递归调用,进入下一层
撤销选择
1.递归树
已选数,可选数,start标记当前选择列表的起始位置,也就是每一层的状态
eg: 第一层可选数为1,2,3, 从start=0都可以选。
第二层对于第一层选择了1的分支,可选数为2,3,从start=1都可以选。第二层分支选了2,从start=2都可以选。
2.结束条件
很特殊,所有路径都应该加入结果集,所以不存在结束条件。或者说start参数越过数组边界的时候,程序自己跳过下一层递归,因此不需要手写结束条件,直接加入结果集。
3.选择列表
子集选择列表,是上一条选择路径之后的数?
4.判断是否需要剪枝
从递归树中看,路径没有重复,也没有不符合的条件,所以不需要剪枝
5.做出选择,for循环里面
5.撤销选择
class Solution {
public List<List<Integer>> subsets(int[] nums) {
// 递归树
// 结束条件:start参数越过数组,自动跳到下一层递归
// 选择列表 nums
// 判断是否需要剪枝 没有重复,也没有不符合的条件,
// 做出选择,for循环里
// 撤销选择
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length == 0) {
return res;
}
Stack<Integer> path = new Stack<>();
int start = 0;
dfs(nums, start, path, res); // dfs 不断向下一层搜索,start记录这一层的状态,表示到达这一层,从哪个位置开始选择,
// 深度方向,不断向下,start + 1; 每一层:都有一个循环,循环里面又有要走nums.length个dfs
return res;
}
private void dfs(int[] nums, int start, Stack<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList(path)); // 每一个分支进入下一层时,将当前path添加到res中
for(int i = start; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i + 1, path, res);
path.pop();
}
}
}
- 这个不是在每一次记递归结束后,将path添加到res中,而是
class Solution {
public List<List<Integer>> subsets(int[] nums) {
// 递归树,每一层有两种选择,选或不选nums[i];
// 递归结束条件:有nums.length层,到达nums.length层就向上弹回溯,而for那一种不是全部深入到nums.length层向上回溯,而是根据
// 选择列表:不选,或nums[i], 二选
// 判断是否需要剪枝:有重复,但是到达叶子节点记录,叶子节点是所有答案
// 做出选择:
// 撤销选择,回溯
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length == 0) return res;
Stack<Integer> path = new Stack<>();
dfs(nums, 0 , path, res);
return res;
}
private void dfs(int[] nums, int index, Stack<Integer> path, List<List<Integer>> res) {
if(index == nums.length) {
res.add(new ArrayList(path));
return;
}
dfs(nums, index+1, path, res);
path.add(nums[index]);
dfs(nums, index + 1, path, res);
path.pop();
}
}
// 递归树: 层: 这一层的每个节点都有nums.length - start个选择; 递归树:层:这一层的每个节点都有选与不选nums[i]
// 结束条件: start到达nums.length结束; path.length到达nums.length
// 剪枝:没有重复,不需剪枝; 叶子节点才统计,叶子节点没有重复,无需剪枝
// 选择列表:从start后顺序选择; 选或不选nums[i]
参考:https://leetcode-cn.com/problems/subsets/solution/hui-su-python-dai-ma-by-liweiwei1419/