【算法攻坚】回溯模板解题

简介: 【算法攻坚】回溯模板解题

今日题目


给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。


candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。


对于给定的输入,保证和为 target 的唯一组合数少于 150 个。


示例 1:


输入: candidates = [2,3,6,7], target = 7 输出: [[7],[2,2,3]]


示例 2:


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


示例 3:


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


示例 4:


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


示例 5:


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


提示:

1 <= candidates.length <= 30 1 <= candidates[i] <= 200 candidate 中的每个元素都是独一无二的。 1 <= target <= 500


思路


这道题是典型的回溯思想寻找最优解的题目,所以还是尝试按照通用模板去解题

解决一个回溯问题,实际上就是一个决策树的遍历过程。只需要思考 3 个问题:


  • 1、路径:也就是已经做出的选择。
  • 2、选择列表:也就是你当前可以做的选择。
  • 3、结束条件:也就是到达决策树底层,无法再做选择的条件。


伪代码实现方面,回溯算法的框架:


result = []
backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
for 选择 in 选择列表:
    做选择
    backtrack(路径, 选择列表)
    撤销选择


这个模板的核心就是 for 循环里面的递归,在递归调用之前做选择,在递归调用之后撤销选择,特别简单。


第一步实现全排列输出:


  • 1、路径:用有序链表存储
  • 2、选择列表:也就是要遍历的元素数组
  • 3、结束条件:链表长度达到最大的数组长度


List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」这里的 选择列表 即包含在nums中
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中的元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择,返回上一层决策树
        track.removeLast();
    }
}


通过上述模板代码实现后,只是实现全排列组合输出,


下一步实现,等于正整数 target的全排列:


  • 1、路径:用有序链表存储
  • 2、选择列表:也就是要遍历的元素数组
  • 3、结束条件:链表中元素之和等于target
  • 4、剪枝:元素排序后,如果当前元素大于target,后面也不会再满足,所以直接结束


public List<List<Integer>> res =  new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    LinkedList<Integer> track = new LinkedList<>();
    // 排序的原因是在回溯的时候比较容易剪枝
    Arrays.sort(candidates);
    // 套用上面的公式,candidates是指选择列表,target用来判断是否结束以及用于剪枝
    // track则是路径,即已经做出的选择
    backtrack(candidates, target, track);
    return res;
}
private void backtrack(int[] candidates, int target, LinkedList<Integer> track) {
    //先判断结束条件
    if (target < 0) {
        return;
    }
    if (target == 0){
        // 当target等于0的时候,将路径加入到结果列表中
        res.add(new LinkedList<>(track));
        return;
    }
    // 遍历选择列表,这里没有去重
    //下面会设置start,每次递归的时候只在candidates中当前及之后的数字中选择。
    for(int i = 0;i < candidates.length;i++){
        // 这就是排序的好处,可以直接这样剪枝,否则还得遍历
        if(target < candidates[i]){
            break;
        } 
        track.add(candidates[i]);
        backtrack(candidates,target-candidates[i],track);
        track.removeLast();
    }
}


上述代码实现了等于target的组合输出,但是并没有达到题目要求的去除重复组合的要求,输出的结果中会有重复的组合元素,所以下一步考虑去重,也就是每次不再从0开始递归元素,都是从当前节点往后进行选取,这样就不会再出现获取前面已经组合过的元素


public List<List<Integer>> res =  new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    LinkedList<Integer> track = new LinkedList<>();
    Arrays.sort(candidates);
    backtrack(candidates, 0, target, track);
    return res;
}
private void backtrack(int[] candidates, int start, int target, LinkedList<Integer> track) {
    if (target < 0) {
        return;
    }
    if (target == 0){
        res.add(new LinkedList<>(track));
        return;
    }
    //int i= start去重组合的关键
    for(int i = start;i < candidates.length;i++){
        if(target < candidates[i]) {
            break;
        }
        track.add(candidates[i]);
        backtrack(candidates,i,target-candidates[i],track);
        track.removeLast();
    }
}


执行用时:2 ms, 在所有 Java 提交中击败了99.65%的用户

内存消耗:38.8 MB, 在所有 Java 提交中击败了21.05%的用户


小结


回溯算法解题整体都是有着类似的目的,都是在寻找所有可行解的题,我们都可以尝试用搜索回溯的方法来解决。同时要记住算法实现的基本框架,可以大大减少实现难度。


相关文章
|
6天前
|
算法 Java Serverless
第十四届蓝桥杯集训——练习解题阶段(无序阶段)-ALGO-444 算法训练 求和问题
第十四届蓝桥杯集训——练习解题阶段(无序阶段)-ALGO-444 算法训练 求和问题
34 1
|
6天前
|
算法 Java C语言
第十四届蓝桥杯集训——练习解题阶段(无序阶段)-ALGO-439 算法训练 简单字符变换
第十四届蓝桥杯集训——练习解题阶段(无序阶段)-ALGO-439 算法训练 简单字符变换
41 1
|
6天前
|
算法 安全
死锁相关知识点以及银行家算法(解题详细步骤)
死锁相关知识点以及银行家算法(解题详细步骤)
8 2
|
6天前
|
算法 决策智能 索引
数据结构与算法 回溯
数据结构与算法 回溯
7 1
|
6天前
|
算法 JavaScript
算法(分治、贪心、dp、回溯、分支限界)总结
算法(分治、贪心、dp、回溯、分支限界)总结
|
6天前
|
算法
算法系列--递归,回溯,剪枝的综合应用(3)(下)
算法系列--递归,回溯,剪枝的综合应用(3)(下)
18 0
|
6天前
|
存储 算法
算法系列--递归,回溯,剪枝的综合应用(3)(上)
算法系列--递归,回溯,剪枝的综合应用(3)(上)
24 0
算法系列--递归,回溯,剪枝的综合应用(3)(上)
|
6天前
|
算法
回溯算法练习题
回溯算法练习题
13 0
|
6天前
|
算法 Java 定位技术
【数据结构与算法】递归、回溯、八皇后 一文打尽!
【数据结构与算法】递归、回溯、八皇后 一文打尽!
|
6天前
|
算法 决策智能
深度探讨回溯算法:追寻解空间的奇妙之旅
深度探讨回溯算法:追寻解空间的奇妙之旅