打卡round1/day1/回溯算法1-组合

1. 关于回溯

回溯/递归,本质上是把问题抽象成一个树型结构,然后用树的思路来解决。树是一种思考工具,我们说到树,是基于树这种数据排列方式来进行数据的处理,而不是呆板地视作一种固定的数据结构再来解决它。

回溯的应用场景

1. 组合问题

2. <待施工>

回溯三要素

1. 目的:穷举整棵树的path,并记录.add(path...)

  1. 注意:path是引用,无论path属于哪种数据结构,都不能直接result.add(path),需要新建一个list:new ArrayList<>(path)

2. 停止:碰到叶节点停止,或碰到目标节点<剪枝>

3. 停止后要回退

  1. 若path为LinkedList:path.removeLast();注意可能需要result.add(new ArrayList<>(path));
  2. 若path为ArrayList: path.remove(path.size()-1)
  3. 若path为StringBuilder: path.deleteCharAt(index)

2. 例题

1. #lc77组合

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

前置思路

        以[1,2,3,4]中取2个数的组合为例,将排列组合结果以树的方式陈列出来,就会发现,需要记录所有长度为k的path,即套娃遍历for循环要进行k次,每次遍历则需要涵盖本层树上的所有节点,即n/n-1/n-2/...个节点

        *问:怎么确保遍历的时候不重复收集之前已经录入path的节点?——用startIndex来锚定每次递归开始的位置

三步思路

1. 本次递归需要哪些参数?返回值是什么?

  • 一维数组path,用来记录每一种组合方式,即遍历组合结果这颗树里的每条路径
  • 二维数组result,储存所有的path,并最终作为答案返回【确定类的返回值】【递归返回值:void】
  • int n:每层递归涵盖的节点数
  • int k:需要套娃遍历的次数,也是path的长度
  • int startIndex:每次递归开始的位置

2. 终止条件?

  • 碰到叶节点停止,即path的长度==k时,并把这条path记录进result

3. 本层树递归时的逻辑怎么写?

  • 录入path新元素:本层当前节点值
  • 递归调用自身:进行【套娃for遍历】,并【修改递归起始值】
  • 记得【回退】

代码实现

class Solution {
    //定义全局变量path和result
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtrack(n,k,1);
        return result;
    }

    public void backtrack(int n, int k, int startIndex){
        //终止条件:碰到叶节点,path记录完毕,录入result
        if(path.size()==k){
            result.add(new ArrayList<>(path));
        }

        //每层递归:套娃for循环,直到遍历完这一层的每个节点到n为止,并注意移动每次递归时的开始位置
        for(int i = startIndex; i<=n; i++){
            path.add(i);
            backtrack(n,k,i+1); //注意这里以i作为移动标尺而不是startIndex,后者是一个固定值
            path.removeLast();
        }
    }
}

2. #lc216组合总和(同一集合,无重复元素,不可重复)

思路

同上

易错点

注意回溯path时sum也要回退

代码实现

class Solution {
    public List<List<Integer>> result = new ArrayList<>();
    public LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        backtrack(k,n,0,1); //注意start值
        return result;
    }

    public void backtrack(int k,int n, int sum, int start){
        if(path.size()==k && sum==n){
            result.add(new ArrayList<>(path));
            return;
        }

        if(path.size()>k || sum>n){
            return;
        }

        for(int i=start; i<=9;i++){
            path.add(i);
            sum+=i;
            backtrack(k,n,sum,i+1);
            
            path.removeLast();
            sum-=i; //注意回溯的时候sum也要回退
        }
    }
}

3. #lc37 组合总和(同一集合,可以重复)

思路

可以无限重复:没有path长度的限制,不需要考虑每次套娃循环的时候前进一位了,直接调用递归函数自己

但是:需要考虑递归的起始位置!<待施工>

代码实现

class Solution {
    public List<List<Integer>> result = new ArrayList<>();
    public List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtrack(candidates,target,0,0);
        return result;
    }

    public void backtrack(int[]candidates, int target,int sum, int start){
        //停止:如果==sum或candidates的遍历超出其长度
        if(sum==target){
            result.add(new ArrayList<>(path));
            return;
        }
        if(sum > target){
            return;
        }


        //注意:这里仍然需要判定递归的起始位置start,但是递归调用自己的时候不必将起始值+1了,可以从自身开始递归
        //每一次做组合,都要考虑起始位置的问题!(如果是多个集合互相搭配则不用)
        for(int i=start;i<candidates.length;i++){
            sum+=candidates[i];
            path.add(candidates[i]);
            //这里难道是一直递归自己吗?——对
            backtrack(candidates,target,sum,i); 
            sum-=candidates[i];
            path.remove(path.size()-1);
        }

    }
}

4. #lc 组合总和(同一集合,有重复数值但不能重复)

思路

同上,但此时需要考虑不能重复

易错点

注意:有重复元素就意味着,可能他们会和同一个元素搭配重复!如[1,1,7]中的1分别和7搭配。如何去重?sort+当前值与前一位比较是否重复

注意:在result.add()的时候,不能直接添加path这个<引用>,需要新建一个list new ArrayList<>(path)

代码实现

class Solution {
    public List<List<Integer>> result = new ArrayList<>();
    public List<Integer> path = new ArrayList<>();
    

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtrack(candidates,target,0,0);
        return result;
    }

    public void backtrack(int[] candidates, int target, int sum, int start){
        if(sum==target){
            //注意不能直接添加path,这样的话path的结果会随着每次递归的变化而变
            result.add(new ArrayList<>(path)); 
            return;
        }
        
        
        if(sum>target){
            return;
        }

        for(int i=start;i<candidates.length;i++){
            //把当前值和前一位比较看是否重复;注意如果start=0会有-1的问题
            if(i>start && candidates[i]==candidates[i-1]){ 
                continue;
            }
            sum+=candidates[i];
            path.add(candidates[i]);
            backtrack(candidates,target,sum,i+1);
            sum-=candidates[i];
            path.remove(path.size()-1);
        }
    }
}

5. #lc17 电话号码组合(不同集合,字符串)

前置知识点(字符串)

  • 获取字符串长度:str.length()
  • 获取字符串某索引位的值:str.charAt(index)
  • 将数字字符转为整型数字:'stringnum'-'0' (如'1'-'0'=int 1)
  • 用stringbuilder快速拼接字符串:
    • 新增:sb.append()
    • 删除:sb.deleteCharAt(index)
    • 获取长度:sb.length()

思路

  1. 建立一个map,存放不同数字与26位字母字符之间的映射关系,注意0-1不在里面
  2. 确定遍历的层数和每层宽度
    1. 分割digits,测量它的长度,即path的长度,即递归(停止)的层数
    2. 确定每层递归遍历节点的数量,即每个数字对应的字符数量。注意:这里不是固定为3!
  3. 进行递归
    1. 确定参数:一个path存储每次路径——用stringbuilder,一个result存储所有path——list<string>,一个字典strMap存储所有数字和字母的映射关系——string[] 直接用默认index作为key去映射字母value
    2. 停止条件:digits里的每个数字都遍历到了/path的长度和digits长度一致
    3. 每次都提取每个数字对应字符串索引位上的字母
    4. 将提取出来的字母组合进path——用stringbuilder
    5. 记得回退——用stringbuilder

易错点

注意如果digits为空或者非数字字符串的情况

代码实现

class Solution {
    public List<String> result = new ArrayList<>();
    public StringBuilder strPath = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        //注意:这里要设digit为空或非数字字符串的情况
        if(digits == null || digits.length() == 0){ 
            return result;
        }
        String[] strMap = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        backtrack(digits,strMap,0);
        return result;
    }

    public void backtrack(String digits,String[] strMap, int index){
        //停止:index已经遍历到了digits的最后一位
        if(index==digits.length()){
            result.add(strPath.toString());
            return;
        }

        /**把digits中的每一位提取出来,通过strMap找到对应的字母组,产出str
        a.进行索引定位时:用-'0'的ASCII码相减,将数字字符转化为整型数字
        b.获取str字母组后:此时获得的是一个多胞胎,要遍历的时候还要再来一遍.charAt(index)
        */
        String str = strMap[digits.charAt(index)-'0'];
        
        //再提取str中的每一个字母并拼接之,再递归到下一层,找digits的下一位数字并重复上述动作
        for(int i=0; i<str.length();i++){
            strPath.append(str.charAt(i));
            backtrack(digits,strMap,index+1);
            strPath.deleteCharAt(strPath.length()-1);//记得回退,删除最后一个数字
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值