文章目录
第77题. 组合
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new LinkedList<>();
public void backtracking(int n,int k,int startIndex){
if (path.size() == k){
result.add(new ArrayList<>(path.subList(0,k))); // 这样,path删除到,result不会删除
return;
}
for (int i=startIndex;i<=n;i++){
path.add(i);
backtracking(n,k,i+1); // 前面的不用管了,从后面的i+1个开始吧
path.remove(path.size()-1); // 因为path要放到result里面,所以要及时删除掉path中元素
}
}
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);// result是类变量,已经被修改,没有必要接收
return result;
}
}
用回溯代替暴力破解解决组合问题
1、对于输入参数中,有一个参数被用作了for循环的个数(即组合的元素个数),此时用递归的来模拟这个for循环
2、接上面的1,因为path要放到result里面,所以要及时删除掉path中元素
3、从1-n,所以初始的时候第三个参数传入进去的就是1,for循环结束条件就是 i<=n
4、因为组合不用考虑顺序,所以第 i 个仅从第 i+1 个开始组合就可以了,如何从startIndex开始的话会出现重复的,但是path是一个list,无法去重。
剪枝操作
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
剪枝(path.size的剪枝)
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new LinkedList<>();
public void backtracking(int n,int k,int startIndex){
if (path.size() == k){
result.add(new ArrayList<>(path.subList(0,k))); // 这样,path删除到,result不会删除
return;
}
for (int i=startIndex;i<=n-(k-path.size())+1;i++){
path.add(i);
backtracking(n,k,i+1); // 前面的不用管了,从后面的i+1个开始吧
path.remove(path.size()-1); // 因为path要放到result里面,所以要及时删除掉path中元素
}
}
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);// result是类变量,已经被修改,没有必要接收
return result;
}
}
第216题.组合总和III
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new LinkedList<>();
public void backtracking(int n,int k,int sum,int startIndex){
if (path.size() == k ){
if(sum==n){
result.add(new ArrayList<>(path.subList(0,k))); // 这样,path删除到,result不会删除 找到了一组合适的,添加到result中
}
return; // 只要path.size到了k个,不能相加是不是sum,都要return;给递归子调用,去path.remove
}
for (int i=startIndex;i<=9;i++){ // 为什么这里是9,因为输出的组合result里面只能含有1-9,所以path中只能含有1-9,只要遍历1-9
sum = sum +i;
path.add(i);
backtracking(n,k,sum,i+1); // 前面的不用管了,从后面的i+1个开始吧 不能用重复的就是组合而不是排序,所以下一次从i+1开始就好
sum=sum-i;
path.remove(path.size()-1); // 因为path要放到result里面,所以要及时删除掉path中元素
}
}
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(n,k,0,1); // result不用接收,类变量已经被修改
return result;
}
}
注意1:result不用接收,类变量已经被修改;
注意2:不能用重复的就是组合而不是排序,所以下一次从i+1开始就好
注意3:因为path要放到result里面,所以要及时删除掉path中元素,核心:每个子组合元素个数为k,要及时删除
注意4:为什么这里是9,因为输出的组合result里面只能含有1-9,所以path中只能含有1-9,只要遍历1-9
注意5:只要path.size到了k个,不能相加是不是sum,都要return;给递归子调用,去path.remove
上一个题目涉及两个,
- 每个子组合和中k个元素,子组合不允许重复(是组合而不是排序,每次从 i+1 开始)
- 一共n个数字,就是为 1 2 3 4 5 … n ,在这个大组合里面选择子组合
这个题目涉及三个,
- 每个子组合和中k个元素,子组合不允许重复(是组合而不是排序,每次从 i+1 开始);
- 一共9个数字,就是为 1 2 3 4 5 … 9 ,在这个大组合里面选择子组合
- 新增:选出来的子组合必须等于给定的n
剪枝操作(总额的剪枝 + path.size的剪枝)
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new LinkedList<>();
public void backtracking(int n,int k,int sum,int startIndex){
if(sum > n) // 总额的剪枝
return;
if (path.size() == k ){
if(sum==n){
result.add(new ArrayList<>(path.subList(0,k))); // 这样,path删除到,result不会删除 找到了一组合适的,添加到result中
}
return; // 只要path.size到了k个,不能相加是不是sum,都要return;给递归子调用,去path.remove
}
// path.size 的剪枝
for (int i=startIndex;i<=9-(k-path.size())+1;i++){ // 为什么这里是9,因为输出的组合result里面只能含有1-9,所以path中只能含有1-9,只要遍历1-9
sum = sum +i;
path.add(i);
backtracking(n,k,sum,i+1); // 前面的不用管了,从后面的i+1个开始吧 不能用重复的就是组合而不是排序,所以下一次从i+1开始就好
sum=sum-i;
path.remove(path.size()-1); // 因为path要放到result里面,所以要及时删除掉path中元素
}
}
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(n,k,0,1); // result不用接收,类变量已经被修改
return result;
}
}
第39题. 组合总和(大组合中,不包含相同元素,但是每个元素可以重复使用 + 不限制k,不限制循环层数或递归次数)
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
关键:candidates 中的数字可以无限制重复被选取,每个数字不限制次数
这个题目涉及三个,
第一,k 每个子组合的元素个数,不确定,但是,子组合不允许重复(是组合而不是排序,每次从 i+1 开始);
第二,大组合,从哪里选择,确定了 candidates
第三,target 就是 n,确定了
有两个不同,
第一,每个子组合中元素个数,不确定,所以不用判断 path.size,也不用根据path.size 剪枝操作,只要根据sum来退出,只要根据sum来剪枝,这个处理了;
第二,母组合中的,candidates中的元素是不重复的,但是可以重复使用,这个怎么处理??
代码如下:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path=new ArrayList<>();
// 前两个是Solution函数带有的参数,没法变为类变量;startIndex是每次要改变,无法作为类变量,虽然此题目中不修改,可以类变量,sum每次修改
public void backtracking(int[] candidates, int target,int sum,int startIndex){
if (sum > target)
return;
if (sum == target){ // target仅仅判断
result.add(new ArrayList<>(path)); // 这样path被删除,result不会被删除
return;
}
// startIndex作为for循环起点,保证子组合元素不重复,如果i=0,开始会重复的,而且存放在List中无法去重
for (int i=startIndex;i<candidates.length;i++){ // 因为给定的大组合是数组,所以从0到n-1
sum = sum + candidates[i]; // candidate仅仅给数据
path.add(candidates[i]); // 末尾添加一个元素
backtracking(candidates,target,sum,i); // 重复执行一个元素不行,才会移动到下一个元素 i++ 之后 startIndex=i,相当于移动了startIndex
sum = sum - candidates[i];
path.remove(path.size()-1); // 删去末尾元素
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates,target,0,0); // 因为给定的大组合是数组,所以从0到n-1 如果给定一个n数字,可以从1到n
return result;
}
}
适当的剪枝操作
&& sum + candidates[i] <= target;
对于大组合,之前两个题目都是从1-n,所以递归子函数里面的for 从startIndex到n(包括),
但是,这个题目,大组合是给定的一个candidates,所以递归子函数中的for 循环从 0到candidates.size(不包括)
第40题. 组合总和 II(大组合中,包含相同元素,但是每个元素只能使用一次 + 不限制k,不限制循环层数或递归次数)
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path=new ArrayList<>();
// 前两个是Solution函数带有的参数,没法变为类变量;startIndex是每次要改变,无法作为类变量,虽然此题目中不修改,可以类变量,sum每次修改
public void backtracking(int[] candidates, int target,int sum,int startIndex,boolean[] used){
if (sum > target)
return;
if (sum == target){ // target仅仅判断
result.add(new ArrayList<>(path)); // 这样path被删除,result不会被删除
return;
}
for (int i=startIndex;i<candidates.length;i++){ // 因为给定的大组合是数组,所以从0到n-1
if (i>0 && used[i-1]==false && candidates[i-1]==candidates[i]) // i-1 没有被使用,i 不会被使用, i-1 被使用了,则使用 i
continue; // i>0 放在前面,保证后面的 i-1 不会数组越界 i<candidates.length 保证 i 不会越界
sum = sum + candidates[i]; // candidate仅仅给数据
path.add(candidates[i]); // 末尾添加一个元素
used[i]=true; // i 号已经被使用
backtracking(candidates,target,sum,i+1,used);
used[i]=false; // 这句是必须的
sum = sum - candidates[i];
path.remove(path.size()-1); // 删去末尾元素,因为path要放到result里面
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // 先排序,排序是必须,让相同元素相邻
boolean[] used=new boolean[candidates.length];
for (int i=0;i<used.length;i++)
used[i]=false; // 全部初始化为false,表示尚未使用
backtracking(candidates,target,0,0,used); // 因为给定的大组合是数组,所以从0到n-1 如果给定一个n数字,可以从1到n
return result;
}
}
第一,需要排序;
第二,需要有一个used数组,初始均为false