回溯(leetcode77)组合类问题

文章介绍了回溯算法的基本思想,它是解决组合问题的有效方法,通过递归和循环避免大量嵌套循环。文章提供了组合问题的代码模板,并展示了如何使用回溯算法解决77.组合问题。此外,还讨论了剪枝优化策略,以提高算法效率,并给出了电话号码字母组合和组合总和II等应用示例。
摘要由CSDN通过智能技术生成

回溯算法的思想

回溯从本质上来说就是穷举法,为了解决一些单纯用嵌套循环解决不了的或者很难解决的题。比如,从0~n中找出长度为k的所有组合。(组合是不在意顺序的,即{1,2}={2,1})

如果k是2,则只需要两层循环就可以列出所有的可能了。但是如果k是50呢,难道要嵌套50层循环吗。如果是100呢,100000呢。显然是不现实的。

所以,我们引入了回溯的概念,内部实现就是在一层循环里嵌套一个递归来代替多层循环。

循环用来控制一层内的遍历,递归控制深度的遍历

回溯的代码模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
​
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合问题

77. 组合

根据回溯类问题的代码模板来写

用res来装最终结果,path装每次递归到递归边界得出的值。这两个要定义为全局变量,放在参数中会让整个代码的可读性变差

List<List<Integer>> res;
List<Integer> path;

  1. 返回值和参数:

    返回值一般都是void,参数包括n和k还有一个index。因为递归需要一个起始位置,所以需要一个index来记录,每次的深度加一就用index来记录。

    public void backtracking(int n, int k, int index){...}

  2. 递归边界

    当path的长度等于k的时候,递归结束,将path加入到res结果集中。

    if(path.size() == k) {
        res.add(new ArrayList<>(path));//此处要new一个ArrayList放入结果集中,如果直接放path,放的是path的地址,后面的操作会让结果集中的数值发生变化
        return ;
    }

  3. 递归体

    用循环遍历一层,用递归遍历深度,用index控制递归起始位置

    for(int i = index; i <= n; i++) {
        path.add(i);
        backtracking(n,k,i+1);
        path.remove(path.size() - 1);
    }

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtracking(n,k,1);
        return res;
    }
    public void backtracking(int n, int k, int index){
        //递归边界
        if (path.size() == k) {
            res.add(new ArrayList<>(path));
            return;
        }
        //循环控制一层内的遍历
        for (int i = index; i <= n; i++) {
            //当前节点进入路径内
            path.add(i);
            //递归控制深度遍历
            backtracking(n,k,i+1);
            //回溯,让path回到进入本次递归前的状态,移除最后一个元素
            path.remove(path.size()-1);
​
        }
    }
}

剪枝优化

其实每次训话并不用都遍历到n采停止,比如上例中第一层的取4就没有必要,因为k为2,但是取4最后的path中只能有一个数,可以直接忽略不考虑。则我们只需在循环的结束控制语句做剪枝即可。

  1. 已经选择的元素个数:path.size();

  2. 还需要的元素个数为: k - path.size();

  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

将n改为n - (k - path.size()) + 1即可

for (int i = index; i <= n - (k - path.size()) + 1; i++) {
            //当前节点进入路径内
            path.add(i);
            //递归控制深度遍历
            backtracking(n,k,i+1);
            //回溯,让path回到进入本次递归前的状态,移除最后一个元素
            path.remove(path.size()-1);
​
        }

类似题目

17. 电话号码的字母组合

本题其实就是组合问题的一种,只不过这种应用题在具体实现细节上可能回有些不知道怎么写的地方。

class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder path = new StringBuilder();
    String[] letterMap = {//用来做数字到字母的映射
        "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
    };
​
    public List<String> letterCombinations(String digits) {
        if(digits == null || digits.length() == 0) {//如果不判断可能会出现输入为"",输出为[""],但是输出应该是[]
            return res;
        }
        backtracking(digits,0);
        return res;
    }
    
    //index用来表明现在递归到digits中的哪个位置了
    public void backtracking(String digits, int index) {
        if (path.length() == digits.length()) {
            res.add(new String(path));
            return ;
        }
        int num = digits.charAt(index) - '0';//要把ASCII码转化为实际值的int数值
        for (int i = 0; i <letterMap[num].length(); i++) {
            path.append(letterMap[num].charAt(i));
            backtracking(digits, ++index);
            index--;
            path.deleteCharAt(path.length()-1);
        }
​
    }
}

40. 组合总和 II

本题和39. 组合总和的区别就在于数据集中是有重复数字的,但是结果集中的结果是不能重复的,所以本题繁琐的地方就是怎么样才能去重。

例如:candidates = [10,1,2,7,6,1,5], target = 8。如果结果集合不去重,得出的结果是

[[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]],但是题目要求的结果应该是

[[1,1,6],[1,2,5],[1,7],[2,6]]

可以发现多出了`[1,2,5]`和`[1,7]`两个重复的集合。

那我们把candidates = [10,1,2,7,6,1,5]中所有重复的元素去除,只留一个变成candidates = [10,2,7,6,1,5]可以吗?

显然是不可以的,因为这样回把结果[1,1,6]给排除掉。所以我们不能对数据集有什么改动。

那我们是应该对整个树的树枝去重还是树层去重呢?

应该是树层去重,树枝去重的话,会把[1,1,6]中的两个1给去掉,从而丢掉一个path结果。

去重的一般步骤:

  • 先对数据集排序

  • candidates[i] == candidates[i-1]的数据,要跳过本次循环,不做操作。

  • 避免树枝上的去重:定义used数组,让used[i-1]=false才进行去重,used[i-1]=true的,即已经被选进path中的,就不做去重

代码

class Solution {
    //需要避免结果集中有重复元素,需要对树层去重
    //先排序,再去重
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path   = new ArrayList<>();
    boolean[] used;
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        used = new boolean[candidates.length];
        Arrays.sort(candidates);
        backtracking(candidates,target,0);
        return res;
    }
​
    public void backtracking(int[] candidates, int target, int startIndex) {
        //剪枝
        if(sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            if (i > 0 && candidates[i] == candidates[i-1] && used[i-1] == false) continue;
            path.add(candidates[i]);
            sum+=candidates[i];
            used[i] = true;
            backtracking(candidates, target, i+1);
            used[i] = false;
            sum-=candidates[i];
            path.remove(path.size()-1);
        }
    }
}

但其实,也可以不用used数组,直接利用starIndex避免树枝去重也可以

代码

class Solution {
    //需要避免结果集中有重复元素,需要对树层去重
    //先排序,再去重
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path   = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracking(candidates,target,0);
        return res;
    }
​
    public void backtracking(int[] candidates, int target, int startIndex) {
        //剪枝
        if(sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            if (i > startIndex && candidates[i] == candidates[i-1]) continue;
            path.add(candidates[i]);
            sum+=candidates[i];
            backtracking(candidates, target, i+1);
            sum-=candidates[i];
            path.remove(path.size()-1);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值