算法笔记(java)——回溯篇

文章详细介绍了回溯算法的原理和应用,包括如何在解决组合、排列和去重问题中使用回溯法。通过具体的力扣题目,如组合总和II和递增子序列,解释了去重策略,如used数组和HashSet的使用。此外,还提到了排列和组合的区别以及相关问题的解题思路。
摘要由CSDN通过智能技术生成

回溯算法解决问题最有规律性,借用一下卡哥的图:
在这里插入图片描述

只要遇到上述问题就可以考虑使用回溯,回溯法的效率并不高,是一种暴力解法,其代码是嵌套在for循环中的递归,用来解决暴力算法解决不了的问题,即可以通过回溯控制递归的层数,递归后可以进行回溯操作,这样下一次循环就不会收到上一次的影响。解决回溯问题的关键就在于清楚整个回溯递归过程,毕竟是嵌套在for循环中的递归,最好的办法就是把所有情况画成一个树,这样就比较清晰了。

递归通用模版和常用API

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

常用API

  • temp常用LinkedList,因为可以在回溯的时候快速删除最后一个元素,添加使用add,删除使用removeLast
  • 将temp添加进result时,要new一个新的arrayList将temp传入构造器,因为temp是全局共享的,如果不new那么result里没有值
  • 如果需要下一层递归指向下一个数,那么要加参数startIndex,如果是排列问题等,每次从头开始,就不需要了

如何用stringbuilder做回溯,我想到了一个办法:添加之前计算一下长度 然后delete就行了 左开右闭 之前sb的最后一个字符是len-1

		int preSize = sb.length();
		sb.append(root.val);
        sb.append("->");
        backTracking(root.left,sb,res);
        backTracking(root.right,sb,res);
        sb.delete(preSize,sb.length());

回溯简单示例

力扣题目:

第77题. 组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

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

暴力for循环是没法解决本题的,因为不知道要写多少层,所以使用回溯法。
本题图解:
在这里插入图片描述
本题代码:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> temp = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracking(n,k,1);
        return result;
    }

    public void backTracking(int n,int k,int startIndex){
        if(temp.size() == k){
            result.add(new ArrayList<Integer>(temp));
            return;
        }
        for(int i=startIndex;i<=n-(k-temp.size())+1;i++){
            temp.add(i);
            backTracking(n,k,i+1);
            temp.removeLast(); //  回溯
        }
    }
}

去重问题

去重是使用回溯算法时遇到的最重要的问题,卡哥将去重分为了树枝去重以及数层去重,就是for循环的去重和各层递归的去重,一个横向一个纵向。

最常用的去重思路:
先对原数组排序,使用used数组,对横向for循环去重,对纵向递归不去重。

力扣题目:

40. 组合总和 II

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

candidates 中的每个数字在每个组合中只能使用 一次 。

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

示例 1:

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

本题图示:
在这里插入图片描述
可以看到只要排序好,进行横向去重就行了。used数组的作用就是去重的时候不要误删纵向情况,因为used数组有回溯操作,下一次for循环的used[i-1]是false了已经,而下一层递归是true,以此为条件进行判断。

class Solution {
     // 去重,去重横向的,纵向递归的不去重,要用used[i-1]防止纵向的被去重的时候误删
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> temp = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if(candidates == null && candidates.length == 0){
            return null;
        }
        Arrays.sort(candidates);
        boolean[] used = new boolean[candidates.length];
        backtracking(candidates,target,0,0,used);
        return result;
    }

    public void backtracking(int[] candidates,int target,int sum,int startIndex,boolean[] used){
        if(sum == target){
            result.add(new ArrayList<>(temp));
            return;
        }
        for(int i=startIndex;i<candidates.length && sum+candidates[i]<=target;i++){
            if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false){
                continue;
            }
            sum += candidates[i];
            temp.add(candidates[i]);
            used[i] = true;
            backtracking(candidates,target,sum,i+1,used);
            used[i] = false;
            temp.removeLast();
            sum -= candidates[i];

        }
    }
}

还有用used数组去重不了的情况
Set去重
力扣题目:

491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

这道题显然是不能用used数组的,因为没法排序,排序后就判断不了递增子序列了。使用hashset或者数组进行去重

class Solution {
    // 不能排序的去重,使用hashset
     List<List<Integer>> result = new ArrayList<>();
     LinkedList<Integer> temp = new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums,0);
        return result;
    }

    public void backTracking(int[] nums,int startIndex){
        if(judge(temp)){
            result.add(new ArrayList<>(temp));
        }
        HashSet<Integer> set = new HashSet<>();
        for(int i= startIndex;i<nums.length;i++){
            if(set.contains(nums[i])){
                continue;
            }
            set.add(nums[i]);
            temp.add(nums[i]); 
            backTracking(nums,i+1);
            temp.removeLast();
        }
    }

    public boolean judge(List<Integer> list){
        if(list.size()<2){
            return false;
        }
        for(int i=0,j=0;j<list.size()-1;i++){
            j=i+1;
            if(list.get(i)>list.get(j)){
                return false;
            }
        }
        return true;
    }
}

每层递归都有自己的hastSet,然后可以对横向for循环去重,因为每一层递归set都不一样,所以不会影响到纵向递归。使用数组同理,和set差不多。

其他问题

排列和组合的区别:

排列是每次递归都是从头开始的:
力扣题目:

46. 全排列

在这里插入图片描述

使用used数组是来判断这个元素有没有在上层递归使用过,毕竟每次都是从0开始

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> temp = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        boolean[] used =new boolean[nums.length];
        backTracking(nums,used);
        return result;
    }

    public void backTracking(int[] nums,boolean[] used){
        
        if(temp.size() == nums.length){
            result.add(new ArrayList<>(temp));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i] == true){
                continue;
            }
            temp.add(nums[i]);
            used[i] = true;
            backTracking(nums,used);
            used[i] = false;
            temp.removeLast();
        }
    }
}

78.子集和90.子集II属于子集问题,这类问题的特点就是要采集树上每一个出现过的元素,不难,去重逻辑也是上面那样。

还有一些拼接字符串的,需要用StringBuilder和一些stringApi的题目:
131.分割回文串和93.复原IP地址 不算难但是比较恶心。

比较难的就是:
51. N皇后
需要遍历矩阵,但是只需要一个for循环,判定条件比较多
52. 解数独
需要双层循环,判定条件也比较复杂

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值