经典回溯算法问题 ----------组合问题

组合问题常见的解法就是回溯算法,通过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:上述所有代码本人都在力扣上进行过测试,并且每题都附上了链接,可以去自己敲着试试。
  • 本文章 ----纪念第一次想明白回溯和组合问题。
  • 3
    点赞
  • 1
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:护眼 设计师:闪电赇 返回首页

打赏作者

换名换一年

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值