回溯算法

1. 特点
  • 效率:纯暴力算法
    说明:在多个for循环才能够解决的问题,或者说是根本由for循环等解决不出来的问题,我们就需要使用回溯算法了,在组合问题中我们可以清楚的了解到某些复杂的问题使用回溯问题较为简洁并且能够求出解。
  • 解释:回溯算法
    回溯与递归是分不开的,简单来说,主要就是在递归的过程中能够弹出一些东西,达到回溯的目的,这个递归也可以理解为dfs深度优先算法,即从一个节点往一个分支一直遍历到底。
2. 解决问题
  • 组合问题(无顺序)
  • 切割问题(字符串)
  • 子集问题(回文子串)
  • 排列问题(有顺序)
  • 棋盘问题(n皇后)
3. 算法说明

前言:这个算法是有一定模板的,我们先来看个伪代码:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //1.调用backTracking
        backTracking(参数);
        return;
    }
    public void backTracking(2.参数){
        //3.跳出递归条件if
        if(){
            //得到我们需要的数据
            return;
        }
        //4.for循环进行的每个元素遍历,其中包含递归backTracking和弹出元素
        for(){
            list.add();	//添加需要的数据
            backTracking(); //递归遍历所有数据,是往下还是当前当前数据重复使用
            list.remove(); //弹出元素,达到回溯的效果,比如已经有{1,2}了,还有组合{1,3},我们就
            			   //需要弹出2,然后在{1}中加3,达到{1,3}的效果
        }
    }
}

算法的注意点:

  • 参数。backTracking()里面需要的参数,包含哪些,可以在做的过程中添加所需要的参数
  • 递归结束条件。比如结果需要的是k=3个数的组合,我们就可以通过判断k等于3来结束
  • for循环条件。需要遍历的数据的条件
  • 递归实现backTracking。比如可以使用重复的该数据,我们可以写成backTracking(,i,);从i开始即当前元素可以重复利用,如果当前元素不能够重复利用,我们就可以backTracking(,i+1,);从下一个元素开始,表示当前元素不可重复利用
  • 弹出。回溯的主要功能,即每个子解每添加一个元素,再下一次递归的时候前弹出它,得到我们需要的结果
  • 剪枝。有些时候,某些递归不需要遍历结束后才终止,我们可以通过剪枝进行提前结束,提高算法时间复杂度,通常在for循环里面实现,比如for (; i <= 9 - (k - temp.size()) + 1; ),这里就表示一个样式
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 。
仅有这两种组合。
示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:

输入: candidates = [2], target = 1
输出: []

简单阐释:就是找candidates里面一些元素和等于目标值target的所有组合。这些元素可以重复使用。

我们要注意几个点:

  • 元素可重复使用
  • 每一个子解长度不一
  • 我们需要有一个变量增减与target进行值比较

代码如下:

class Solution {
    //全局变量
    List<List<Integer>> res = new ArrayList<>();		//输出集合res
    List<Integer> List = new ArrayList<>();				//子解集合list
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates.length == 0 || candidates == null){
            return res;
        }
        backTracking(candidates, target, 0, 0);		//1.调用backTracking,注意里面的参数
        //包括前两个我们需要的,和第1个0表示开始下标,即candidates数组下标从0开始,第2个0表示sum和,初始值为0,后面需要和target进行比较
        return res;
    }
    public void backTracking(int[] candidates, int target, int stratIndex, int sum){
        if (sum > target){		//sum和大于target就递归结束,return不要忘了
            return;
        }
        if (sum == target){
            res.add(new ArrayList<>(List));		//找到我们需要的子解,添加进去
            return;
        }
        for (int i = stratIndex; i < candidates.length; i++){
            List.add(candidates[i]);	//添加元素进list
            sum += candidates[i];		//每添加一个,sum就需要增和
            backTracking(candidates, target, i, sum);	//递归,注意为i,而不是i+1,表示当前可重复
            List.remove(List.size() - 1);//递归结束,找到一个子解,然后弹出最后一个元素,可看上面伪代码解释
            sum -= candidates[i];	//如上面弹出,只弹出最后一位,即sum需要和减最后一位
        }
    }
}
5.总结

刚开始我学习回溯是有点不好理解的,毕竟比较抽象,所以需要理清我们不理解的有哪几个点,然后就是借鉴别人的代码,以及看别人是如何解释该问题点的,然后通过多做几道回溯题进行增加印象,达到后面能够自己写一些简单回溯题的目的。