回溯算法重要的是能理清楚回溯的树形结构,优化时一般使用剪枝操作。
回溯三部曲:(1)递归函数的参数 (2)递归的终止条件 (3)单层搜索的逻辑
一、集合里面的元素每个只可以使用一次的组合问题
1. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
树形结构:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>(); // 这个就是要在末尾插入元素与删除元素
public List<List<Integer>> combine(int n, int k) {
// 返回的是组合,就不需要考虑元素的顺序
// 返回1 - 4中所有元素的组合
backTracking(n,k,1);
return result;
}
private void backTracking(int n, int k, int startIndex) {
// 终止条件
if(path.size() == k) { // 收集
// 创建了一个新的ArrayList对象然后存入 result中
result.add(new ArrayList<>(path));
return ;
}
// 当前回溯函数开始的位置,即到第几个元素了
// for(int i = startIndex; i <= n; i++)
// 剪枝:必须现在集合中需要4个元素,path中已经收集了1元素,之后只剩两个元素可以选择了,此时很明显不能成功
// 所以可以把这一段给砍掉,防止它继续往下面搜素
for(int i = startIndex; i <= n-(k-path.size()) + 1; i++) {
path.add(i);
backTracking(n,k,i+1);
path.removeLast(); // 移除最后一个元素
}
}
}
2. 组合总和Ⅲ
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
这里的变量sum也可以使用n不断减去每个元素来代替,也可以把sum放到回溯函数的参数中。
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
// 每个数字只使用一次,求的还是组合
backTracking(n,k,1);
return result;
}
// 参数; n为相加之和, 用sum记录当前元素的和
private void backTracking(int n, int k, int startIndex) {
if(sum > n) {
return;
}
if(sum == n && path.size() == k) {
result.add(new ArrayList<>(path));
return ;
}
for(int i=startIndex; i <= 9 - (k - path.size()) + 1; i++){
// 如果当前这轮sum 大于9就return
sum += i;
path.add(i);
backTracking(n,k,i+1);
sum -= i;
path.removeLast();
}
}
}
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(n, k, 1, 0);
return result;
}
private void backTracking(int targetSum, int k, int startIndex, int sum) {
// 减枝
if (sum > targetSum) {
return;
}
if (path.size() == k) {
if (sum == targetSum) result.add(new ArrayList<>(path));
return;
}
// 减枝 9 - (k - path.size()) + 1
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backTracking(targetSum, k, i + 1, sum);
//回溯
path.removeLast();
//回溯
sum -= i;
}
}
}
// 上面剪枝 i <= 9 - (k - path.size()) + 1; 如果还是不清楚
// 也可以改为 if (path.size() > k) return; 执行效率上是一样的
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
build(k, n, 1, 0);
return ans;
}
private void build(int k, int n, int startIndex, int sum) {
if (sum > n) return;
if (path.size() > k) return;
if (sum == n && path.size() == k) {
ans.add(new ArrayList<>(path));
return;
}
for(int i = startIndex; i <= 9; i++) {
path.add(i);
sum += i;
build(k, n, i + 1, sum);
sum -= i;
path.removeLast();
}
}
}
3. 电话号码的字母组合
集合里面的元素每个可以使用多次的组合问题
4. 组合总和
-- 无重复元素数组,可无限使用,目的是相加获得目标整数
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates =[2,3,6,7]
, target =7
输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
注意:1. 主要是要学会如何处理重复的情况,因为如果设置了startIndex ,那样就变成了每一个元素只可以使用一次;如果不设置startIndex 而是让起始位置为下标0,就会出现重复集合的情况,比如[2,2,3] 和 [2,3,2]会同时出现;
如何解决这种情况呢,就是要让程序选择到当前元素时就不可以选择之前的元素,这种操作需要通过startIndex来进行控制;而对于当前元素可以重复选择操作则是需要通过递归使用dfs时传入的startIndex参数来控制,如果传入的i+1,则变成了上一类的问题,这里应当设置为i,当前元素索引即可。
2. 本题中的剪枝操作是容易出错的,在剪枝前应当先进行排序。具体原因见代码注释。
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>(); // 收集单层结果
public List<List<Integer>> combinationSum(int[] candidates, int target) {
/**
1. 回溯函数的参数
2. 递归终止条件
3. 单层搜索的逻辑
*/
// 为什么要先排序呢?
// 这里进行排序是为了后面的剪枝操作:(sum+candidates[i] <= target);
// 如果不排序,虽然sum + candidates[i] 大于 target ,但是不可以保证 sum+candidates[i+1]以及后面的数也会超过target
Arrays.sort(candidates);
dfs(candidates,target,0,0);
return result;
}
// 要传入数组,目标值,单结合操作(加上startIndex), 当前的sum和
private void dfs(int[] candidates, int target, int startIndex, int sum) {
if(sum == target) {
// 收集结果
// 注意,这里不可以使用下面的代码
// result.add(path); // 会出现所有的path都是空
//在收集结果时,result.add(path)只是将path的引用添加到结果列表中。
// 这可能会导致在后续的回溯过程中,path的变化会影响已经添加到结果列表中的内容。
// 解决方法是在添加到结果列表时,创建一个新的列表并复制path的内容。修改后的代码如下:
result.add(new ArrayList<>(path));
return ;
}
if(sum > target) {
return ; // 终止
}
// sum < target
// 这里不可以从 startIndex开始了吧,因为每个元素都可以选择;
// 如果不设置startIndex 又会出现一个重复的问题
for(int i = startIndex;i < candidates.length && (sum+candidates[i] <= target); i++) {
// 选择元素
sum += candidates[i];
path.add(candidates[i]);
dfs(candidates,target,i,sum);
// 回溯
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
集合里面的元素只能使用一次,但是集合中有重复元素——去重的逻辑
5. 组合总和Ⅱ
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates =[10,1,2,7,6,1,5]
, target =8
, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
由于数组中存在重复的元素,如果只使用startindex(像1.组合问题一样), 那么最后就会出现重复集合的问题
以下有两种去重的方式:
一. 保留startIndex,但是得先将数组排序,这样做的目的是为了让相同的元素挨到一起,然后使用
candidates[i] == candidates[i - 1]
。。。 回来再写