代码随想录算法营|回溯

基础知识

1. 回溯和递归是相辅相成,递归函数的下面就是回溯的逻辑,也就是回溯=递归+for循环

2. 回溯搜索算法:暴力搜索

  • 组合问题(没有顺序):一般递归的时候会设置startindex记录遍历到哪个数字,递归的时候用i,收获的结果是叶子节点
  • 切割问题:收获的结果是叶子节点,有判断是否满足切割的条件,需要startindex
  • 子集问题:收获的结果是每个点,需要startindex
  • 排列问题(有顺序):不同的元素顺序,是不同的组合,不需要startindex,每次从头搜索;可以用path.contains代替used数组
  • 棋盘问题

3.如何理解回溯法:都可以抽象为一个n叉树形结构,宽度就是每个结点要处理的集合大小(用for循环遍历),深度就是递归的深度

4. 回溯法backtracking的模板:一般来说都没有返回值,参数比较多边用边添加

题目一:组合问题:

1. 题目:77. 组合 - 力扣(LeetCode)   :给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合

2. 思路:画出一个树形结构,得到的答案不能重复,设置一个startindex控制每次搜索的起始位置

  • 参数是path(一维数组放中间遍历,全局变量)、res(二维数组放答案、全局变量)、n是一共有几个数,k是答案的长度、startindex搜索的起始位置;返回值是void
  • 终止条件:path的大小为k if(path.size==k){result.add(path); return}
  • 回溯逻辑:第一层每一个结点都是一个for循环,从startindex开始遍历剩下的元素,元素放入path,再从i+1递归,再把元素从path弹出

3. 注意的点:

  • path添加ans中要先if (path.size()==k)ans.add(new ArrayList<>(path))
  • 全局变量得是静态的才能被引用
  • 剪枝操作:优化for循环,还需要选k-path.size()个元素,最多要从n-(k-path.size())+1开始搜索。比如n=4,k=3,当选了0个元素的时候,4-(3-0)+1=2,最多从2开始往后找比如234,从3开始只有34不符合要求。
class Solution {
     List<List<Integer>> ans=new ArrayList<>();
     List<Integer> path=new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        int index=1;
        backtrack(n,k,index);
        return ans;
    }

    public   void backtrack(int n,int k,int index){
        if (path.size()==k){
            ans.add(new ArrayList<>(path));
            return;
        }
        for(int i=index;i<=n - (k - path.size()) + 1;i++){
            path.add(i);
            backtrack(n,k,i+1);
            path.remove(path.size()-1);
        }
    }
}

 题目二:电话号码的字母组合

1. 题目:17. 电话号码的字母组合 - 力扣(LeetCode) 

2. 数字到字符串的映射,二维数组或者map,用递归替代for循环也就是答案的个数

3. 递归返回值void,参数是字符串,index表示当前递归遍历到哪个数字(和前两题不一样)(startindex用于在一个集合标记收获哪些元素)

终止条件:if(index==digits.size())result.add(path)表明遍历到头了,再return

单层遍历逻辑:

先找到字符串中每个数字对应的字母集,遍历这个字母集,比如说输入的是2,对应的是[a,b,c]就遍历[a,b,c]

放入到中间结果集中;再递归

class Solution {

    //设置全局列表存储最后的结果
    List<String> list = new ArrayList<>();

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return list;
        }
        //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        //迭代处理
        backTracking(digits, numString, 0);
        return list;

    }

    //每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
    StringBuilder temp = new StringBuilder();

    //比如digits如果为"23",num 为0,则str表示2对应的 abc
    public void backTracking(String digits, String[] numString, int num) {
        //遍历全部一次记录一次得到的字符串
        if (num == digits.length()) {
            list.add(temp.toString());
            return;
        }
        //str 表示当前num对应的字符串
        String str = numString[digits.charAt(num) - '0'];
        for (int i = 0; i < str.length(); i++) {
            temp.append(str.charAt(i));
            //递归,处理下一层
            backTracking(digits, numString, num + 1);
            //剔除末尾的继续尝试
            temp.deleteCharAt(temp.length() - 1);
        }
    }
}

题目三:组合总和|||(数组无重复,答案中k个数字不重复)

1. 题目:216. 组合总和 III - 力扣(LeetCode)  :找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字

2. 代码:记得剪枝

class Solution {
      int sum=0;
     List<List<Integer>>ans=new ArrayList<>();
     List<Integer> path=new ArrayList<>();
    public  List<List<Integer>> combinationSum3(int k, int n) {
        backtrack(k,n,1);
        return ans;
    }

    public  void backtrack(int k,int n,int start){
        if (sum==n && path.size()==k){
            ans.add(new ArrayList<>(path));
            return;}
        if (sum>n || path.size()>k)return;
        for (int i=start;i<=9-(k-path.size()+1);i++){
            sum+=i;
            path.add(i);
            backtrack(k,n,i+1);
            sum-=i;
            path.remove(path.size()-1);
        }
    }
}

题目四:组合总和(数组无重复,答案中数字可重复)

1. 题目:39. 组合总和 - 力扣(LeetCode) 集合里的元素可以重复选,给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合

2. 回溯三部曲

  • 返回值是void,参数:candidate、target、sum、startindex,全局变量:二维数组ans、一维数组path
  • 边界条件:如果sum>target return,如果sum==target 将path插入到ans中 return
  • 单层搜索逻辑:for(int i=startinex;i<candidates.length;i++)
class Solution {
    int sum=0;
    List<List<Integer>>ans=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public  List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtrack(candidates,target,0);
        return ans;
    }

    public  void backtrack(int[] nums,int target,int index){
        if (sum==target){
            ans.add(new ArrayList<>(path));
            return;
        }
        if (sum>target)return;
        for (int i=index;i<nums.length;i++){
            path.add(nums[i]);
            sum+=nums[i];
            backtrack(nums,target,i);//不是index因为index一直是0
            path.removeLast();
            sum-=nums[i];
        }
    }
}

题目五:组合总和||(数组有重复,答案中数字不重复)

1. 题目: 40. 组合总和 II - 力扣(LeetCode) 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合

2. 思路:先将数组进行排序,比如{1,1,2}

树层去重:第二个1包含的情况第一个1已经遍历过,所以第二个1不用遍历剪枝

树枝去重

3. 思路

  • 全局变量:path一维数组、result二维数组;返回值void;参数:nums、target、sum、startindex、used数组标记哪些元素用过
  • 边界条件:sum>target就return,sum==target就加入到ans中return
  • 单层搜索:用for循环
    • 树层去重:
    • if(i>0 && nums[i]==nums[i-1] && used[i-1]==0) continue;跳出本次for循环
    • used[i-1]==0就是说已经遍历完第一个1,开始遍历第二个1;如果used[i-1]==1表明再第一个1的分支里不能去重
class Solution {
     int sum=0;
     List<List<Integer>>ans=new ArrayList<>();
     List<Integer> path=new ArrayList<>();
     boolean[] used;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        used=new boolean[candidates.length];
        Arrays.fill(used,false);
        backtrack(candidates,target,0);
        return ans;
    }

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

题目六:分割回文串

1.题目:131. 分割回文串 - 力扣(LeetCode) 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串(substring是左闭右开)

2. 思路

  • 全局变量是ans、path;返回值void;参数是字符串、startindex控制切割的位置
  • 终止条件:切割到最后一个字符串starxindex>=s.length 就将path插入到ans中,startxiindex就是切割的线
  • 单层搜索逻辑:判断是否是回文,不是的话不会进行下一层递归
    • 子串的范围是:(startindex,i]左闭右开,startindex是固定的
    • 判断是否是回文的操作

class Solution {
    int sum=0;
     List<List<String>>ans=new ArrayList<>();
     List<String> path=new ArrayList<>();

    public List<List<String>> partition(String s) {
        backtrack(s,0);
        return ans;
    }

    public void backtrack(String s,int startindex){
        if (startindex==s.length()){
            ans.add(new ArrayList<>(path));
            return;
        }
        for(int i=startindex;i<s.length();i++){
            if(huiwen(s,startindex,i)==true) {
                path.add(s.substring(startindex,i+1));//substring是左闭右开的
                backtrack(s,i+1);
                path.remove(path.size()-1);
            }else continue;
        }
    }
    public boolean huiwen(String s,int start,int end){
        while (start<=end){
            if (s.charAt(start)!=s.charAt(end))return false;
            else {
                start++;
                end--;
            }
        }
        return true;
    }
}

题目七:复原IP地址

1. 题目:93. 复原 IP 地址 - 力扣(LeetCode) 

2. 思路:因为数字过大,所以不能用Integer.parseint

  • 全局变量:ans;返回值void;参数是:s、startindex(切割线来规定下一层递归从哪开始)、pointsum(加上.来分割)
  • 终止条件:if(pointsum==3){}也就是树的深度是3,逗点是对前一个区间的字符串的合法性判断
    • 对最后一个进行合法性判断:isvalid(s,startindex,s,size()-1)是左闭右闭区间,判断数字前面有没有0,有没有超过255,没有非法字符
    • 最后将s放入结果集,再return
  • 递归逻辑:for(int i=startindex;i<sisize()-1;i++)
    • 对放入的字符串进行合法性判断:[startindex,i]左闭右闭
      • 将子字符串的后面加上.后,pointsum++
      • 下一层递归backtrack(s,i+2,pointsum)因为加了一个逗点
      • 回溯:删除逗点,pointsum--
class Solution {
 List<String> ans=new ArrayList<>();
    public  List<String> restoreIpAddresses(String s) {
        StringBuilder sb=new StringBuilder(s);
        backtrack(0,0,sb);
        return ans;
    }

    public void backtrack(int startindex,int sumpoint,StringBuilder sb){
       if (sumpoint==3){
           if (isvalid(sb,startindex,sb.length()-1)) {
               ans.add(sb.toString());
           }
           return;//不管怎么样都返回
       }
       for (int i=startindex;i<sb.length();i++){
           if (isvalid(sb,startindex,i)){
               sb.insert(i+1,'.');
               sumpoint++;
               backtrack(i+2,sumpoint,sb);//回溯i+2的位置
               sb.deleteCharAt(i+1);
               sumpoint--;
           }else continue;
       }
    }
    public boolean isvalid(StringBuilder s,int start,int end){
        if(start > end) return false;
        if(s.charAt(start) == '0' && start != end) return false;
        //else if (Integer.parseInt(s.substring(start,end).toString())>255)return false;
        int num = 0;
        for(int i = start; i <= end; i++){
            int digit = s.charAt(i) - '0';
            num = num * 10 + digit;
            if(num > 255)
                return false;
        }
        return true;
    }
}

题目八:子集问题(每个点都要收获结果不光是叶子)

1. 题目:78. 子集 - 力扣(LeetCode)  每进入一层递归,就要把本层里的结果放入结果集

2. 思路:

  • 全局变量:path、ans;返回值void;参数是数组、startindex
  • 终止条件:先将path放入ans,剩余集合都是空也就是startindex>=nums.length return
  • 单层搜索逻辑:for(int i=1;i<nums.length();i++)
    • 路上的点都要push进path中,递归下一层,回溯
class Solution {
     List<Integer> path=new ArrayList<>();
     List<List<Integer>> ans=new ArrayList<>();
    public  List<List<Integer>> subsets(int[] nums) {
        backtrack(nums,0);
        return ans;
    }
    public void backtrack(int[] nums,int start){
        ans.add(new ArrayList<>(path));//放入结果的时候要new ArrayList
        if (start>=nums.length)return;
        for (int i=start;i<nums.length;i++){
            path.add(nums[i]);
            backtrack(nums,i+1);
            path.removeLast();
        }
    }
}

题目九:子集||

1. 题目:90. 子集 II - 力扣(LeetCode) 

2. 注意:要先判断i>0再判断nums[i-1]==nums[i];used是全局变量不能再函数中重新定义

class Solution {
     List<Integer> path=new ArrayList<>();
     List<List<Integer>> ans=new ArrayList<>();
     boolean[] used;
    public  List<List<Integer>> subsetsWithDup(int[] nums) {
        used=new boolean[nums.length];
        Arrays.fill(used,false);
        Arrays.sort(nums);
        backtrack(nums,0);
        return ans;
    }

    public  void backtrack(int[] nums,int start){
        ans.add(new ArrayList<>(path));
        if (start>=nums.length)return;
        for (int i=start;i<nums.length;i++){
            if (i>0&&nums[i]==nums[i-1] &&used[i-1]==false )continue;
                path.add(nums[i]);
                used[i]=true;
                backtrack(nums,i+1);
                used[i]=false;
                path.removeLast();
        }
    }
}

题目十:递增子序列

1. 题目:代码随想录 (programmercarl.com) ,结果分布在节点上

2. 误区:不能将nums进行排序,会有其他没有的子集出现

3. 思路

  • 全局变量:path、ans;返回值void;参数:nums、startindex
  • 终止条件:if(path.size>1)ans.add(new ArrayList(path)) ;if(startindex==nums.length) return 
  • 递归逻辑: for(int i=0; i<nums.length;i++)
    • 跳出条件:if(nums[i]<path.last() && path不为空 && uset中没有nums[i]) continue;
    • map更新:将nums[i]放入map中;因为记录的是每一层是否取过,所以不需要回退
class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums, 0);
        return result;
    }
    private void backTracking(int[] nums, int startIndex){
        if(path.size() >= 2)
                result.add(new ArrayList<>(path));            
        HashSet<Integer> hs = new HashSet<>();
        for(int i = startIndex; i < nums.length; i++){
            if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i]))
                continue;
            hs.add(nums[i]);
            path.add(nums[i]);
            backTracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

题目十一:全排列

1. 题目:46. 全排列 - 力扣(LeetCode) 

2. 代码:

class Solution {//有used数组
       List<Integer> path=new ArrayList<>();
     List<List<Integer>> ans=new ArrayList<>();
     boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
        used=new boolean[nums.length];
        Arrays.fill(used,false);
        backtrack(nums);
        return ans;
    }
    public  void backtrack(int[] nums){
        if (path.size()==nums.length)
        {ans.add(new ArrayList<>(path));
            return;}
        for (int i=0;i<nums.length;i++){
            if (used[i]==false) {
                path.add(nums[i]);
                used[i]=true;
                backtrack(nums);
                used[i]=false;
                path.removeLast();
            }
        }
    }
}
class Solution {//没有used数组
    List<Integer> path=new ArrayList<>();
    List<List<Integer>> ans=new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        backtrack(nums);
        return ans;
    }
    public  void backtrack(int[] nums){
        if (path.size()==nums.length)
        {ans.add(new ArrayList<>(path));
            return;}
        for (int i=0;i<nums.length;i++){
            if (!path.contains(nums[i])) {
                path.add(nums[i]);
                backtrack(nums);
                path.removeLast();
            }
        }
    }
}

题目十二:全排列||(nums中有重复元素)

1. 题目:47. 全排列 II - 力扣(LeetCode) 

2. 代码:

class Solution {
      List<Integer> path=new ArrayList<>();
     List<List<Integer>> ans=new ArrayList<>();
     boolean[] used;
    public  List<List<Integer>> permuteUnique(int[] nums) {
        used=new boolean[nums.length];
        Arrays.fill(used,false);
        Arrays.sort(nums);
        backtrack(nums);
        return ans;
    }
    public  void backtrack(int[] nums){
        if (path.size()==nums.length){
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int i=0;i<nums.length;i++){
            if (i>0 && nums[i]==nums[i-1] && used[i-1]==false )continue;
            if (used[i]==false) {
                path.add(nums[i]);
                used[i] = true;
                backtrack(nums);
                path.removeLast();
                used[i] = false;
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值