回溯:02组合问题合集

组合问题

为什么要用回溯?

组合题:已知一个集合1,2,3,4,要求将大小为2的集合找出来,两层for很容易得出,但是100个数50个集合呢,如何通过一个数k来控制for循环的层数呢

回溯算法就是递归来控制有多少层for循环

这类题就是把树形结构给想好,套模板即可解决

题目:77. 组合

77. 组合

class Solution {
    LinkedList<Integer> path = new LinkedList<>();//用LinkedList是细节
    List<List<Integer>> result = new ArrayList<>();//可以放到递归函数的参数列表中,但为了提升代码可读性,所以放在类属性里面
    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
    void backtracking(int n, int k, int startIndex) {
        if(path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex; i <= n; i++) {
            path.add(i);
            backtracking(n, k, i + 1);
            path.removeLast();
        }
        return;
    }
}

优化(减枝操作)

来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。

所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

path.size();//已经有的元素个数
k-path.size();//还需要选取的元素个数
this = n-(k - path.size()) + 1;//至多从这里this开始搜索符合条件,如果当前i在this之后就一定不符合条件了,因为i是从startIndex开始包含startIndex,所以需要加1;startIndex到n总共有n-startIndex+1个数,我思考的是右半部分的大小
//为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合[1,2,3,4]

回溯算法如果想要剪枝,就思考for的范围是不是太大了

class Solution {
    LinkedList<Integer> path = new LinkedList<>();//用LinkedList是细节
    List<List<Integer>> result = new ArrayList<>();//可以放到递归函数的参数列表中,但为了提升代码可读性,所以放在类属性里面
    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
    void backtracking(int n, int k, int startIndex) {
        if(path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.add(i);
            backtracking(n, k, i + 1);
            path.removeLast();
        }
        return;
    }
}

限定条件组合 题目:216. 组合总和 III

216. 组合总和 III

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracking(k, n, 1, 0);
        return result;
    }
    void backtracking(int k, int n, int startIndex, int sum) {
        //有多少限制条件,就该有多少剪枝操作
        if(sum > n) return;//剪枝,关于和n的

        if(path.size() == k) {//如果数量够了就没必要往下递归了
            if(sum == n ) {//符合条件
                result.add(new ArrayList<>(path));
            }
            return;
        }
        for(int i = startIndex; i <= 9 - (k -path.size()) + 1; i++) {//组合剪枝:集合.size - (所需数量 - 已经收集.size) + 1
            path.add(i);
            backtracking(k, n, i + 1, sum + i);
            path.removeLast();
        }
    }
}

上面的终止条件path.size()k和sumn这一点若同时在同一个if中,会一直循环stackoverflow;

若每次循环的集合不一致 :17. 电话号码的字母组合

17. 电话号码的字母组合

class Solution {
    //设置全局列表存储最后的结果
    List<String> result = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        if(digits == null || digits.length() == 0) {
            return result;
        }
        String[] numString = {
            "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
        };
        backtracking(digits, 0, numString);
        return result;
    }
    public void backtracking(String digits, int index, String[] numString) {//numString是映射数组
        //设置终止条件:遍历到叶子节点就结束了,也就是说给的字符串遍历完了
        if(index == digits.length()) {
            result.add(sb.toString());
            return;
        }

        //得到循环处理用的字符串
        String s = numString[digits.charAt(index) - '0'];
        for(int i = 0; i < s.length(); i++) {
            sb.append(s.charAt(i));
            backtracking(digits, index + 1, numString);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

若元素可重复选取 :39. 组合总和

39. 组合总和

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracking(candidates, target, 0, 0);
        return result;
    }
    void backtracking(int[] nums, int target, int sum, int startIndex) {
        // 两个终止条件
        //值大于目标值
        if(sum > target) {
            return;
        }
        //找到目的path
        if(sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }

        for(int i = startIndex; i < nums.length; i++) {//横向遍历
        //剪枝,对于排完序的数组,如果我的和已经大于了目标值,那么我后面的层序遍历和下面的纵向遍历都没有必要了
            if((sum + nums[i]) > target) break;
            path.add(nums[i]);
            backtracking(nums, target, sum + nums[i], i);
            path.removeLast();
        }
    }
}

集合中有重复的元素:40. 组合总和 II

40. 组合总和 II

本题集合中有重复的元素,也就是说要进行去重

所有回溯问题都可以抽象成一颗n叉树,想想在n叉树中是怎么去重的–选到重复的元素有两种情况,一种是纵向遍历(树枝也就是一条分支即递归)另一种是横向遍历(树层也就是for循环),对于一个有序数组来说,我们要去重的是树层,因为树枝是纵向来说的,我们选了一个1,递归到下一层如果元素中有1,我们就还能选,但是如果在树层,例如集合[1,1,2,3],遍历到下一个分支,第一次我们选了第二个1,由于前面已经有1了,在上一分支中,第一次选第一个1的时候已经包含了第二次选1的所有分支,甚至连第二个1自己都包含在内,就是说这层的这个分支没有必要存在了,反应在代码中就是在树层中(for循环中),如果前一个数字和本数字一致且上一个数字没用过,就说明本层本分支结束,对于树枝是没必要的,就是上一个数字用过了,那么就是说本循环是在第一个数字1的分支下面,不用去重。

class Solution {
    List<List<Integer>> result = new ArrayList<>();//记录结果
    LinkedList<Integer> path = new LinkedList<>();//记录单条分支
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        //采用标记数组去重一定要记得将数组排序
        Arrays.sort(candidates);
        backtracking(candidates, target, 0, 0, new boolean[candidates.length]);
        return result;
    }
    void backtracking(int[] nums, int target, int sum, int startIndex, boolean[] used) {
        if(sum > target) {//已经大于目标值,本条分支不符合了
            return;
        }
        if(sum == target) {
            result.add(new ArrayList<>(path));
        }

        //回溯逻辑
        for(int i = startIndex; i < nums.length; i++) {
            //进行去重操作
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {//树层方面去重
                continue;
            }

            //一般处理逻辑
            path.add(nums[i]);
            used[i] = true;
            backtracking(nums, target, sum + nums[i], i + 1, used);
            used[i] = false;
            path.removeLast();
        }
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

曦煜墨白

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值