组合问题常见的解法就是回溯算法,通过dfs + 剪枝找出所有符合条件的组合
下面通过几个题看出其中的模板和套路
1.https://leetcode-cn.com/problems/combinations/ 组合问题:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
- 这道题是回溯的经典问题,如果题目仅仅是给出的这个用例,我们完全可以双重循环解决掉,但是将k扩大到100呢,你要写多少循环,这样效率肯定会非常低。但是用回溯就很好解决了,再加上适当的剪枝,效率还会再次提升。
- 那么这题该怎么解决呢?
- 先来看看回溯算法的经典模板
backTrack (需要的一些参数){
//1.递归的终止条件,必须要有,且要放在最开始
if(){
//一些操作(视具体情况而定,可以没有)
return;
}
// ?----->(这里可以理解为,每次的起点,举个例子,如果说明了不能重复,那么我们每次递归都要从当前值之后找)
// ??----->(这里可以理解为,子树的数量)
for(int i = ?; i < ??; i++){
//一些操作(视具体情况而定,可以没有)
//2.递归的调用
backTrack(一些参数);
//一些操作(视具体情况而定,可以没有)
}
}
- 看完模板,来看看这题怎么解决?
- 首先来解释一下,为什么用回溯?
- 这个回溯函数可以理解为,for循环中相当于横向遍历,递归调用相当于纵向遍历,这样就把所有的情况给找完了,通过适当的判断和剪枝,拿到自己想要的答案。每次回溯,触底才会向回反弹。
- 可以把这些想象成一个多叉树,每次都在这些数中选,满足条件了就选出来。
- 所以上述就可以想象成,我们先定义一个结果集合,再定义一个List集合存放每次加的值,开始时先选择1,在选择2,发现k == list.size() ,这样就有一组满足条件了,加入结果集合,接着递归调用,继续寻找满足条件的就行,下面看下代码:(我们dfs套用模板试试)
class Solution {
//定义结果数组
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
List<Integer> list = new ArrayList<>();
//调用
dfs(1, list, n, k);
//返回结果数组
return res;
}
/*
start: 每次从什么位置开始寻找,为了避免重复情况,我们寻找了之后的元素,不能再选择之前的元素,比如你选择4, 就不能选择4之前的数
list: 每次选择所用的集合
n, k : 题目所给条件
*/
public void dfs(int start, List<Integer> list, int n, int k){
//说明找到了一组满足条件的,加入集合
if(list.size() == k){
res.add(new ArrayList<>(list));
return;
}
//递归调用,继续找
for(int i = start; i <= n; i++){
list.add(i);
dfs(i + 1, list, n, k);
//这里因为每次用的都是新数组,为了不污染其他的结果数,我们在回溯的时候要去掉
//另外一种解决方式是,每次都新建一个List,但是这样效率很低
list.remove(list.size() - 1);
}
}
}
- 到这里,对这个题是否有了不一样的看法?回溯的思想就是这样。
- 下面我们再来看一题:
2. https://leetcode-cn.com/problems/combination-sum/ 组合总和问题:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例1 :
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
- 这道题依旧是回溯算法的经典问题。要注意的是,每次进行选择时,下一次的递归调用的target就要变成target - candidates[i],思路依旧和上面那道题类型,就是进行回溯寻找满足条件的即可,有个小小的剪枝,就是当数组中的数 > target,就没有必要继续找下去了,肯定是不满足条件的。下面依旧是套用模板,看看代码:
class Solution {
//定义一个结果数组
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<Integer> list = new ArrayList<>();
//调用
dfs(candidates, target, 0, list);
return res;
}
public void dfs(int[] candidates, int target, int start, List<Integer> list){
//说明找到了一组
if(target == 0){
res.add(new ArrayList<>(list));
return;
}
//递归调用
for(int i = start; i < candidates.length; i++){
//不满足条件的直接排除
if(candidates[i] <= target){
list.add(candidates[i]);
//选了之后,从当前值之后开始选,防止出现重复
dfs(candidates, target - candidates[i], i, list);
//选完减去,避免一直添加
list.remove(list.size() - 1);
}
}
}
}
- 看完这道题,你是不是看出来套路都相差不多呢?别急,再来看一题:
3.https://leetcode-cn.com/problems/combination-sum-ii/ 组合总和问题2:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明:
- 所有数字(包括目标数)都是正整数。
- 解集不能包含重复的组合。
示例1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
- 看完这道题,是不是感觉和刚才的那道题非常的相似,这道题思路大致和前一道题思路相差不多,唯一的难点在于怎么去重?如果再和刚才那样写,会有很多重复答案。这里的去重操作非常的巧妙。其中一种思路是:先将数组进行排序,这样我所有的重复元素都集中在一起了,我再进行处理就很方便了。下面看看代码:
class Solution {
//定义结果集合
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//定义每次满足条件的集合
List<Integer> list = new ArrayList<>();
//排序为了之后去重
Arrays.sort(candidates);
dfs(candidates, target, 0, list);
return res;
}
//dfs
public void dfs(int[] candidates, int target, int start, List<Integer> list){
//说明找到了一组
if(target == 0){
res.add(new ArrayList<>(list));
return;
}
//找的过程
for(int i = start; i < candidates.length; i++){
//满足条件继续加,不满足不进去
if(candidates[i] <= target){
//去重操作
if(i > start && candidates[i] == candidates[i - 1]){
continue;
}
//添加
list.add(candidates[i]);
//从当前值之后开始找,target对应的值也要改变
dfs(candidates, target - candidates[i], i + 1, list);
//防止污染
list.remove(list.size() - 1);
}
}
}
}
- 这里主要谈一下为什么能保证去重(搬自力扣题解liweiwei大佬下的最高赞评论Allen大佬的评论),因为说的太好了
- 至此,这道题也解决了,最后再看一道题:
4.https://leetcode-cn.com/problems/combination-sum-iii/ 组合总和问题3 :找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
- 这道题看起来是否也很熟悉,没错和第一题的又是一个类型的题目。下面直接给出代码,注意考虑重复的因素即可。
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
List<Integer> list = new ArrayList<>();
dfs(k, n, 1, list);
return res;
}
private void dfs(int k, int n, int start, List<Integer> list){
//没有找下去的必要
if(n < 0 || k < 0){
return;
}
//找到一组
if(n == 0 && k == 0){
res.add(new ArrayList<>(list));
return;
}
//递归调用
for(int i = start; i <= 9; i++){
list.add(i);
dfs(k - 1, n - i, i + 1, list);
list.remove(list.size() - 1);
}
}
}
- ps:上述所有代码本人都在力扣上进行过测试,并且每题都附上了链接,可以去自己敲着试试。
- 本文章 ----纪念第一次想明白回溯和组合问题。