一. 简单介绍

1. 回溯算法简单介绍

        回溯算法是基于递归函数之上的,本质是穷举所有可能符合答案的选项。和一般递归最大的区别是,本身带有撤销这一明显特征,即选择,递归,然后返回,撤销。虽然可以加一些剪枝的操作,但本身算法时间复杂度很高,没有改变穷举的本质。

        回溯算法主要解决排列,组合,子集类的问题 [1]。

2. 回溯算法一般模板

public void dfs(参数1,参数2,...,参数n){

    //终止条件
        if(){
        //添加答案
        return;
    }

    //遍历所有可能选项
    for(int i = ?; i < ?; i++){
        //添加操作
        dfs()
        //撤回操作,和有可能的第二次dfs()
        dfs()
    }

}

 3. 回溯算法的树型转换

        回溯算法可以用树来转换,树的深度遍历即DFS可以理解为向下的递归,for循环内可以理解为对所有的可能性的排查,是对本层的遍历。

        以leetcode77中为例,那么对数型结构的转换为:

java 回溯 图 背包问题 java 回溯算法_算法

 以树形结构来思考,可以更具体地抽象出递归的过程,也会方便剪枝。

二. leetcode实战

1. leetcode 77 组合

        给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

输入:n = 4, k = 2
输出:
[
  [2,4],[3,4], [2,3],[1,2], [1,3],[1,4],
]

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        List<Integer> path = new ArrayList<>();
        dfs(1,n,k,path);
        return ans;
    }
    public void dfs(int index, int n, int k , List<Integer> path){
        if(path.size() == k){
            ans.add(new ArrayList<Integer>(path));
            return;
        }
        for(int i = index; i <= n; i++){
            path.add(i);
            dfs(i+1, n , k, path);
            path.remove(path.size() - 1);
        }
    }
}

本题小结:

        本题是经典的回溯问题,要注意的点有:(1)ans.add()中要生成一个新的list

                                                                         (2)dfs(i+1, n , k, path)中是i还是index

                                                                         (3)index产生12组,i产生6组

回溯剪枝:

         观察n=4,k=3的情况,以树形结构给出图:

java 回溯 图 背包问题 java 回溯算法_算法_02

         可以看到,在红线打×的地方可以提前终止,这是因为储备库里的选项已经不足让我们选择出可以构成结果的选项。这意味着我们可以在这些地方进行剪枝。

        n为最大长度,k为目标长度,size为已经选择过的数量。那么,for循环内i的提前终止位置为:

java 回溯 图 背包问题 java 回溯算法_List_03

size即为path中已经产生过的数量。剪枝代码如下:

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        List<Integer> path = new ArrayList<>();
        dfs(1,n,k,path);
        return ans;
    }
    public void dfs(int index, int n, int k , List<Integer> path){
        if(path.size() == k){
            ans.add(new ArrayList<Integer>(path));
            return;
        }
        for(int i = index; i <= n-(k-path.size())+1; i++){
            path.add(i);
            dfs(i+1, n , k, path);
           path.remove(path.size() - 1);
        }
    }
}

         假如k=50,n=100,size=10,这意味着,我们现在已经有10个数了,终止位置变成了100-(50-10)+1=61,那么在61开始,后面都不可能凑出答案,因为,目标数一共50个,现在一共10个,从61个开始后面一共就39个数,39+10=49是凑不够50的。

2. leetcode216 组合总和

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

只使用数字1到9
每个数字 最多使用一次 
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<Integer> path = new ArrayList<>();
        dfs(1,n,k,path);
        return ans;
    }
    public void dfs(int index, int n, int k, List<Integer> path){
        if(path.size() == k){
            int sum = 0;
            for(int i : path){
                sum += i;
            }
            if(sum == n) ans.add(new ArrayList<Integer>(path));
            return;
        }
        for(int i = index; i <= 9; i++){
            path.add(i);
            dfs(i+1,n,k,path);
            path.remove(path.size()-1);
        }
    }
}

剪枝版本:

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<Integer> path = new ArrayList<>();
        dfs(1,n,k,path);
        return ans;
    }
    public void dfs(int index, int n, int k, List<Integer> path){
        if(path.size() == k){
            int sum = 0;
            for(int i : path){
                sum += i;
            }
            if(sum == n) ans.add(new ArrayList<Integer>(path));
            return;
        }
        for(int i = index; i <= 9-(k-path.size())+1; i++){
            path.add(i);
            dfs(i+1,n,k,path);
            path.remove(path.size()-1);
        }
    }
}

本题小结:

        本题和77如出一辙,关键点在于在满足容量要求后和的要求,求一下sum再判断一下即可,剪枝的方法也和77一样。

3. leetcode17 电话号码的字母组合

        给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

java 回溯 图 背包问题 java 回溯算法_List_04

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

class Solution {
    List<String> ans = new LinkedList<>();
    public List<String> letterCombinations(String digits) {
       if(digits.length() == 0) return ans;
        int len = digits.length();
        dfs(digits,0,len,"");
        return ans;
    }
    public void dfs(String digits,int index, int len,String path){
        if(index == len){
            ans.add(path);
            return;
        }
        int num = 3;
        int diff = 0;
        if(digits.charAt(index) == '9'||digits.charAt(index) == '7') num = 4;
        if(digits.charAt(index) == '8'||digits.charAt(index) == '9') diff = 1;
        for(int i = 0; i < num; i++){
            String temp = String.valueOf((char)((digits.charAt(index)-'0')*3+i+91+diff));
            path += temp;
            dfs(digits,index+1,len,path);
            path = path.substring(0,path.length()-1);
        }
    }
}

数组映射版本:

class Solution {
    List<String> ans = new LinkedList<>();
    String[] tel = new String[]{"","",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"};
    public List<String> letterCombinations(String digits) {
        if(digits.length() == 0) return ans;
        dfs(0,digits,"");
        return ans;
    }
    public void dfs(int index,String digits,String path){
        if(path.length() == digits.length()){
            ans.add(path);
            return;
        }
        int num = digits.charAt(index)-48;
        String numstring = tel[num];
        for(int i = 0; i < numstring.length(); i++){
           path += numstring.substring(i,i+1);
           dfs(index+1,digits,path);
           path = path.substring(0,path.length()-1);
        }
    }
}

本题小结:

        本题最关键的点在于(1)建立相应的映射,可大大减少复杂性,但相应的降低复用性

                                        (2)index从0开始向后遍历,在for中循环每一个index对应的string

其它和正常回溯一样。

4. leetcode39 组合总和

        给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。仅有这两种组合。

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
       List<Integer> path = new ArrayList<>();
        dfs(0,target,0,candidates,path);
        return ans;
    }
    public void dfs(int index,int target,int sum,int[] candidates,List<Integer> path){
        if(sum == target){
            ans.add(new ArrayList<>(path));
            return;
        }
        if(index == candidates.length){
            return;
        }
        if(sum > target){
            return;
        }
        for(int i = index; i < candidates.length; i++){
            sum += candidates[i];
            path.add(candidates[i]);
            dfs(i,target,sum,candidates,path);
            path.remove(path.size()-1);
            sum -= candidates[i];
        }
    }
}

剪枝:

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> path = new ArrayList<>();
        Arrays.sort(candidates);
        dfs(0,target,0,candidates,path);
        return ans;
    }
    public void dfs(int index,int target,int sum,int[] candidates,List<Integer> path){
        if(sum == target){
            ans.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i < candidates.length; i++){
            sum += candidates[i];
            if(sum > target) break;
            path.add(candidates[i]);
            dfs(i,target,sum,candidates,path);
            path.remove(path.size()-1);
            sum -= candidates[i];
        }
    }
}

本题小结:(1)为避免取重复数据{2,3}{3,2}所以i从index开始,不能是0

                (2)为了还能取本层,for中的dfs要取i

                (3)本题可以通过先排序然后在for循环中进行剪枝

5. leetcode40 组合总和II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[ [1,1,6],[1,2,5],[1,7],[2,6] ]

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> path = new ArrayList<>();
        Arrays.sort(candidates);
        dfs(0,target,0,candidates,path);
        return ans;
    }
    public void dfs(int index, int target, int sum,int[] candidates,List<Integer> path){
        if(target == sum){
            ans.add(new ArrayList<Integer>(path));
        }
        for(int i = index; i < candidates.length; i++){
            if(i > index && candidates[i] == candidates[i-1]) continue;
            if(sum > target) break;
            sum += candidates[i];
            path.add(candidates[i]);
            dfs(i+1,target,sum,candidates,path);
            path.remove(path.size()-1);
            sum -= candidates[i];
        }
    }
}

本题小结:(1)为避免取重复数据{2,3}{3,2}所以i从index开始,和39一样

                (2)这题不取本层,只能使用一次,所以取i+1

                (3)本题剪枝是sum > target

                (4)特别注意i > index && candidates[i] == candidates[i-1]过滤同层相等

特别的:

java 回溯 图 背包问题 java 回溯算法_java 回溯 图 背包问题_05

 url-> Allen

参考来源:

[1] 代码随想录 卡尔 回溯算法