文章目录
子集问题(所有节点):第78题. 子集,不需要对结果剪枝去重
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集(题目明确给定)
示例: 输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
思路
求子集问题和回溯算法:求组合问题!和回溯算法:分割问题!又不一样了。
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,「那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!」
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
「那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!」
有同学问了,什么时候for可以从0开始呢?
求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path=new ArrayList<>();
public void backtracking(int[] nums,int startIndex){
result.add(new ArrayList<>(path));
if (startIndex >= nums.length){
return;
}
for (int i=startIndex;i<nums.length;i++){ // 从 startIndex 到 nums.length-1 ,第一次进来的时候 startIndex 为0,然后每次+1
path.add(nums[i]); // 元素添加进来
backtracking(nums,i+1);
path.remove(path.size()-1);
}
}
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums,0); // 每次开始,都是从0开始
return result;
}
}
子集问题(所有节点):第90题.子集II(不含重复的集合,和上一次组合问题,元素不允许重复使用是一样的,加上used数组)
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集(题目明确给定)
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
「剧透一下,后期要讲解的排列问题里去重也是这个套路,所以理解“树层去重”和“树枝去重”非常重要」。
用示例中的[1, 2, 2] 来举例,如图所示:(「注意去重需要先对集合排序」)
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path=new ArrayList<>();
public void backtracking(int[] nums,int startIndex,boolean[] used){
result.add(new ArrayList<>(path));
if (startIndex >= nums.length){ // 最后这就是结束条件
return;
}
for (int i=startIndex;i<nums.length;i++){ // 从 startIndex 到 nums.length-1 ,第一次进来的时候 startIndex 为0,然后每次+1
if (i>0 && nums[i]==nums[i-1] && used[i-1]==false)
continue; // 同一个树层使用了就放弃
path.add(nums[i]); // 元素添加进来
used[i]=true;
backtracking(nums,i+1,used);
path.remove(path.size()-1);
used[i]=false;
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
boolean[] used=new boolean[nums.length];
for (int i=0;i<used.length;i++)
used[i]=false;
Arrays.sort(nums); // 对nums快速排序
backtracking(nums,0,used); // 每次开始,都是从0开始
return result;
}
}
核心:对于结果,元素在同一个子组合内是可以重复的,怎么重复都没事,但两子个组合不能相同
大组合中每个元素只能使用一次(非组合的数学特性,题目明确给定条件),且结果中不能包含相同子组合(题目明确给定输出结果要求,子组合中元素个数是无序的):大组合排好顺序之后,相同元素同一个树层中只能使用一次,出现两次,得到的子组合就有重复的了;
大集合中每一个元素只能使用一次(集合的数学特性,因为是集合,所以这样,题目隐含条件),结果中不能包含相同子集合(题目明确给定输出结果要求,子集合中元素个数是无序的):大集合排好顺序之后,相同元素只能使用一次,出现两次,得到的子集合就有重复的了。
递增子序列
大序列中每个元素只能使用一次(序列的数学特性,因为是序列,所以这样,题目隐含条件),结果中不能包含相同子序列(题目明确给定输出结果要求,子序列中元素个数要求是递增的,所以要剪枝if):大序列给定后(不能排序),同一树层相同元素只能使用一次,出现两次,得到的子序列就有重复的了。还有,子序列子序列中元素个数要求是递增的,所以要剪枝if。所以两个剪枝
子序列问题(所有节点):491.递增子序列(子序列问题和子集问题两个不同)
子序列问题和子集问题两个不同
- 第一个剪枝if不同,即去重逻辑不同,子集执行前需要排序,子序列执行前不需要排序;
- 第二个剪枝if不同,子序列多一个剪枝if,就是当前元素不能大于子序列最后一个元素。
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2 。
示例:
输入:[4, 6, 7, 7]
输出:[[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
思想:本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>(); // result和path的java类型,要根据输出的格式来定义
public void backtracking(int[] nums,int startIndex){
if (path.size() > 1){
result.add(new ArrayList<>(path)); // 这就是结束条件,也是成功结束条件,因为没有失败且结束条件,失败条件就是result为[],初始化结果
// 这里不要加return 要取出树上的节点
// 加上return 永远取到的是两个元素的子序列
}
Set set=new HashSet(); // set自带去重功能 在递归的时候去重,不是在for循环中去重
// 在for循环中去重,
// 从startIndex到nums.length-1,i不能从0开始,否则 i+1 没有意义
for(int i=startIndex;i<nums.length;i++){
// 第一个剪枝,不能加入小于的
if (!path.isEmpty() && nums[i]<path.get(path.size()-1))
continue;
// 第二个剪枝,不能包含同一个树层使用过的
if (set.contains(nums[i])==true) // set的定义一定要在for循环外面
continue;
path.add(nums[i]);
set.add(nums[i]);
backtracking(nums,i+1);
path.remove(path.size()-1);
}
}
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return result;
}
}
为什么结束条件中不能加上return;
回答:这样的话,只要满足if (path.size() > 1),就会return到上一个函数,执行path.remove(path.size()-1);永远只能输出包含两个元素的子序列,
如果不加上return; 一定要startIndex == nums.length,才会执行path.remove(path.size()-1);
核心:不能加return; 因为即使满足if (path.size() > 1),仅仅是两个元素的,不能停止,后面还有多个元素的,如果return停止了,就只能取到两个元素的了