Leetcode分类刷算法之回溯专题

Leetcode上必做的回溯算法题目

  • 回溯,回溯的时候减枝,两大类排列和组合问题,总结排列组合类型的各自特点,以及考虑去重!

暴力枚举 回溯

回溯法写法思路:

  1. 定义全局结果数组
  2. 调用递归函数
  3. 返回全局结果数组
  4. 定义递归函数
  1. 参数,动态变化,一般为分支结果、限制条件等
  2. 终止条件,将分支结果添加到全局数组
  3. 剪枝条件
  4. 调用递归逐步产生结果,回溯搜索下一结果

1.组合总和

39. 组合总和

private List<List<Integer>> res;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new ArrayList<>();
        if (candidates == null || candidates.length == 0 ||  target < 0) return res;
        Arrays.sort(candidates);//排序可以提前终止判断排序的主要作用是剪枝
        generateCombination(candidates,0, target, new ArrayList<>());
        return res;
    }

    private void generateCombination(int[] candidates,int start, int target, ArrayList<Integer> temp) {
        if (target == 0){
            res.add(new ArrayList<>(temp)); //Java中的可变对象时引用传递,需要将当前路径里面的值拷贝出来
            return;
        }
        //在循环的时候做判断,target - candidates[i] 表示下一轮的剩余 ,如果下一轮的剩余都小于0了,就没有必要在进行后面的循环了。
        //这一点基于数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小。
        for (int i = start; i < candidates.length && target - candidates[i] >= 0; i++) {
            temp.add(candidates[i]);
            //因为元素可以重复使用,这里递归下去的是i,不是i + 1
            generateCombination(candidates, i, target - candidates[i],temp);
            //java的引用传递,导致list数组一直增多,在每次递归完成之后,要进行一次回溯。把最新加入的那个数删除。
            temp.remove(temp.size() - 1); 
        }
    }

2. 组合总和 II

40. 组合总和 II

 List<List<Integer>> res;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new ArrayList<>();
        if (candidates == null || candidates.length == 0 || target < 0)return res;
        Arrays.sort(candidates);//排序可以提前终止判断
        generateCombination(candidates, 0 , target, new ArrayList<Integer>());
        return res;
    }

    private void generateCombination(int[] candidates, int start, int target, ArrayList<Integer> temp) {
        if (target == 0) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = start; i < candidates.length && target - candidates[i] >= 0; i++) {
            //因为这个数和上个数相同,所以从这个数开始的情况,在上个数里面考虑过了,需要跳过(去重)
            if(i > start && candidates[i] == candidates[i - 1])continue;
            temp.add(candidates[i]);
            //元素不可以重复,这里递归传递下去的是i + 1,而不是i
            generateCombination(candidates, i + 1, target - candidates[i], temp);
            temp.remove(temp.size() - 1);
        }
    }

3. 组合总和 III

216. 组合总和 III

1、尝试做减法,减到 0 就说明可能找到了一个符合题意的组合,但是题目对组合里元素的个数有限制,因此还需要对元素个数做判断;

2、如果减到负数,也没有必要继续搜索下去;

3、因为结果集里的元素互不相同,因此下一层搜索的起点应该是上一层搜索的起点值 + 1;

 List<List<Integer>> res;
    public List<List<Integer>> combinationSum3(int k, int target) {
        res = new ArrayList<>();
        if (target < 0 && k <= 0)return res;
        generateCombination(k , target , 1, new ArrayList<Integer>());
        return res;
    }

    private void generateCombination(int k, int target, int start, ArrayList<Integer> temp) {
        if (target == 0 && temp.size() == k) {
            res.add(new ArrayList<>(temp)); //对象类型的状态变量,在方法传递中是引用传递,在结算的时候需要做一个拷贝
            return;
        }
        // temp.size() <= k 剪枝
        // target - i >= 0 剪枝
        for (int i = start; i <= 9 && temp.size() <= k && target - i >= 0; i++) {
            temp.add(i);
            generateCombination(k, target - i, i + 1, temp);
            temp.remove(temp.size() - 1); //对象类型的状态变量,在方法传递中是引用传递,在回溯的时候,需要重置线程
        }
    }
    

4. 组合总和 Ⅳ

377. 组合总和 Ⅳ

回溯 超时 每次递归都是从0开始的,所有的数字都遍历一遍,就可以出现重复的组合

ArrayList<ArrayList<Integer>> res = new ArrayList<>();
    public int combinationSum4(int[] nums, int target) {
        if (nums == null || nums.length == 0 || target < 0){
            return 0;
        }
        Arrays.sort(nums);
        generationCombinationSum(nums, target, new ArrayList<>());
        return res.size();
    }

    public void generationCombinationSum(int[] nums, int target, ArrayList<Integer> temp) {
        if (target == 0){
            res.add(new ArrayList(temp));
            return;
        }
        for (int i = 0; i < nums.length && target - nums[i] >= 0; i++) {
            temp.add(nums[i]);
            generationCombinationSum(nums, target - nums[i], temp);
            temp.remove(temp.size() - 1);
        }
        return;
    }

动态规划:

Leetcode分类刷算法之回溯专题_第1张图片

重叠子问题,可以使用动态规划来做,如果要求具体的解,可以用“回溯算法”

    //不用得到具体的组合表示,可以用动态规划
    //dp[i] : 数组中和为i的组合的个数
    //dp[i] = dp[i - nums[0]] + dp[i - nums[1]] + dp[i - nums[2]]
    //[1, 3, 4] dp[7] = dp[6] + dp[4] + dp[3]
    public int combinationSum4(int[] nums, int target) {
        if (nums.length == 0)return 0;
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 1; i <= target; i++) {
            for (int j = 0; j < nums.length; j++) {
                if (i - nums[j] >= 0) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }

5. 组合

77. 组合

Leetcode分类刷算法之回溯专题_第2张图片
剪枝
当选定了一个元素,即 pre.size() == 1 的时候,接下来要选择 2 个元素, i 最大的值是 4 ,因为从 5 开始选择,就无解了;
当选定了两个元素,即 pre.size() == 2 的时候,接下来要选择 1 个元素, i 最大的值是 5 ,因为从 6 开始选择,就无解了。

i <= n - (k - temp.size()) + 1
 List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList<>();
        if (n <= 0 || k <= 0 || k > n)return res;
        generationCombine(n, 1 , k, new ArrayList<>());
        return res;
    }

    private void generationCombine(int n, int start, int k, ArrayList<Integer> temp) {
        if (temp.size() == k) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = start; i <= n - (k - temp.size()) + 1; i++) {
            temp.add(i);
            generationCombine(n,i + 1,k, temp);
            temp.remove(temp.size() - 1);
        }
    }

6 全排列

46. 全排列

题解回溯算法详解

for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表移除
    路径.add(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

Leetcode分类刷算法之回溯专题_第3张图片

 List<List<Integer>> res;
    public List<List<Integer>> permute(int[] nums) {
        res = new ArrayList<>();
        if (nums == null || nums.length == 0)return res;
        boolean[] used = new boolean[nums.length];
        generatePermutation(nums, used, new ArrayList<Integer>());
        return res;
    }

    private void generatePermutation(int[] nums, boolean[] used, ArrayList<Integer> temp) {
        if (temp.size() == nums.length) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            temp.add(nums[i]);
            generatePermutation(nums, used, temp);
            used[i] = false;
            temp.remove(temp.size() - 1);
        }
    }

和之前的元素不一样,全排列要求每个元素都必须出现一次,所以需要boolean数组来标记是否访问过了。而且每次循环都是从 i = 0开始

7. 全排列 II

全排列 II

  List<List<Integer>> res;
    public List<List<Integer>> permuteUnique(int[] nums) {
        res = new ArrayList<>();
        if (nums == null || nums.length == 0) return res;
        Arrays.sort(nums);//排序的目的是为了判断重复数字是否选过
        boolean[] used = new boolean[nums.length];
        generatePermutation(nums, used, new ArrayList<Integer>());
        return res;
    }

    private void generatePermutation(int[] nums, boolean[] used, ArrayList<Integer> temp) {
        if (temp.size() == nums.length) {
            res.add(new ArrayList<>(temp));
            return;
        }
        //递归拆解,从选择列表中选择何时条件的数字
        for (int i = 0; i < nums.length; i++) {
            //为了避免重复,只选择第一个重复的数字进行dfs
            //如果当前的数和前一个数相等,判断前一个数是否选择过,如果选择过则跳过,正面前面已经选过重复的数字了
            if (used[i] ||(i > 0 && nums[i] == nums[i - 1] && used[i - 1])) {
                continue;
            }
            used[i] = true;// 将该选择从选择列表移除
            temp.add(nums[i]);//路径.add(选择)
            generatePermutation(nums, used, temp);
            //回溯
            used[i] = false;//将该选择再加入选择列表
            temp.remove(temp.size() - 1);// 路径.remove(选择)
        }
    }

8. 子集

78. 子集

List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList<>();
        if (nums == null || nums.length == 0)return res;
        generateSubsets(nums, 0 , new ArrayList<Integer>());
        return res;
    }

    private void generateSubsets(int[] nums, int start,ArrayList<Integer> temp) {
        res.add(new ArrayList<>(temp));
        for (int i = start; i < nums.length; i++) {
            temp.add(nums[i]);
            generateSubsets(nums, i + 1, temp);
            temp.remove(temp.size() - 1);
        }
    }

9. 子集 II

90. 子集 II
同样是包含重复元素

重复元素先排序,然后在选择列表的时候记得去重

 List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList<>();
        if (nums == null || nums.length == 0)return res;
        Arrays.sort(nums);
        generateSubsets(nums, 0 , new ArrayList<Integer>());
        return res;
    }

    private void generateSubsets(int[] nums, int start, ArrayList<Integer> temp) {
       res.add(new ArrayList<>(temp));
       for (int i = start; i < nums.length; i++) {
           if (i > start && nums[i] == nums[i - 1])continue;
           temp.add(nums[i]);
           generateSubsets(nums, i + 1, temp);
           temp.remove(temp.size() - 1);
       } 
    }

10. 电话号码的字母组合

17. 电话号码的字母组合

public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0)return res;
        map.put('2',"abc");
        map.put('3',"def");
        map.put('4',"ghi");
        map.put('5',"jkl");
        map.put('6',"mno");
        map.put('7',"pqrs");
        map.put('8',"tuv");
        map.put('9',"wxyz");
        generateCombinations(digits,0,"");
        return res;
    }

    private void generateCombinations(String digits, int start, String temp) {
        if (start == digits.length()){
            res.add(temp);
            return;
        }
        Character c = digits.charAt(start);
        String letter = map.get(c);
        for (int i = 0; i < letter.length(); i++) {
            generateCombinations(digits, start + 1,temp + letter.charAt(i));
        }
    }

你可能感兴趣的:(数据结构与算法)