首页 > 编程语言 >代码随想录算法训练营第30天|回溯复习篇

代码随想录算法训练营第30天|回溯复习篇

时间:2024-06-07 19:29:35浏览次数:23  
标签:res nums int 随想录 30 list 回溯 new public

回溯基础理论

1.回溯的本质是利用递归进行暴力搜索,将符和条件的结果集搜索出来

2.回溯法常见的问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等

3.回溯法常见模板

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

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

4.回溯法抽象为一个图形来理解就容易多了在后面的每一道回溯法的题目我都将遍历过程抽象为树形结构,利用树形结构可以形象化回溯的过程,便于理解,所以在解决回溯问题时,可以将二叉搜索树画出来

复习题1

77. 组合 - 力扣(LeetCode)

二叉搜索你树

回溯三部曲:

1.确认递归返回值及参数

    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();

res用于收集最后所有可能的结果,list用于存储符合条件的单一结果

2.回溯终止条件

当list的大小为k时既符合题目条件

 if (list.size() == k) {
            res.add(new ArrayList<>(list));
            return;
        }

3.单层搜索的过程、

从startindex开始进行遍历,将符合条件的数加入到list中后继续向下递归

for循环每次从startIndex开始遍历,然后用list保存取到的节点i。

  for (int i = start; i <= n - (k - list.size()) + 1; i++) {
            list.add(i);
            dfs(k, n, i + 1);
            list.remove(list.size() - 1);
        }

 完整代码:

class Solution {

    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();

    public void dfs(int k, int n, int start) {
        if (list.size() == k) {
            res.add(new ArrayList<>(list));
            return;
        }
        for (int i = start; i <= n - (k - list.size()) + 1; i++) {
            list.add(i);
            dfs(k, n, i + 1);
            list.remove(list.size() - 1);
        }
        return;
    }

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

复习题2

216. 组合总和 III - 力扣(LeetCode)

二叉搜索树

 本体与上题的搜索过程是像相同的,在条件上加了一个和为指定数

回溯三部曲

1.确认递归返回值及参数

    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();

res用于收集最后所有可能的结果,list用于存储符合条件的单一结果

2.回溯终止条件

当list的大小为k,并且和为n时既符合题目条件,或者当前list中元素的和已经大于n时也没有继续搜索的必要了

      if(sum>n){
            return;
        }
        if (list.size() == k&&sum==n) {
            res.add(new ArrayList<>(list));
            return;
        }

3.单层搜索过程

从startindex开始进行遍历,将符合条件的数加入到list中后,sum对其相加后 继续向下递归,回溯时将list最后的数移除,sum进行相减

for循环每次从startIndex开始遍历,然后用list保存取到的节点i。

 for (int i = start; i <= 9; i++) {
            sum += i;// 处理
            list.add(i);

            dfs(n, k, i + 1, sum);// 向下搜

            sum -= i;// 回溯
            list.remove(list.size() - 1);
        }

完整代码:

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

    public  List<Integer> list = new ArrayList<>();

    public  void dfs(int n, int k, int start, int sum) {

        if (sum > n) {
            return;// (如果sum>n那么后面在加就没有意义了)剪枝
        }
        if (list.size() == k && sum == n) {
            res.add(new ArrayList<>(list));// 收集结果
            return;
        }
        for (int i = start; i <= 9; i++) {
            sum += i;// 处理
            list.add(i);

            dfs(n, k, i + 1, sum);// 向下搜

            sum -= i;// 回溯
            list.remove(list.size() - 1);
        }
        return;
    }

    public List<List<Integer>> combinationSum3(int k, int n) {
        int sum = 0;
        dfs(n, k, 1, sum);
        return res;
    }
}

 复习题3

216. 组合总和 III - 力扣(LeetCode)

二叉搜索树

 本题的不同点在于数组中的元素可以多次使用,所以在下次递归时的startIndex不用加一还是从当前元素开始

回溯三部曲

1.确认递归返回值及参数

    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();

res用于收集最后所有可能的结果,list用于存储符合条件的单一结果

2.递归终止条件

当list的元素的和为targetSum时既符合题目条件或者当sum大于targetSum时也没有继续搜索的必要了

  //sum>=target就返回
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(list));
            return;
        }

3.单层搜索过程
从startIndex开始对nums数组进行遍历将nums数组的元素加入到list中后进行向下递归,注意下次的开始位置任然为当前元素位置(本题的元素可以重复选)

  for (int i = start; i < nums.length; i++) {
            list.add(nums[i]);//处理
            sum += nums[i];

            dfs(nums, target, i);//递归,注意这里是i(表示可以重复读取当前的数)

            list.remove(list.size() - 1);//回溯
            sum -= nums[i];
        }

完整代码:

class Solution {
    public  List<List<Integer>> res = new ArrayList<>();
    public  List<Integer> list = new ArrayList<>();
    public  int sum = 0;

     /**
     * @param nums    目标数组
     * @param target  目标和
     * @param start   起始位置
     */
    public  void dfs(int[] nums, int target, int start) {
   
        //sum>=target就返回
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(list));
            return;
        }

        for (int i = start; i < nums.length; i++) {
            list.add(nums[i]);//处理
            sum += nums[i];

            dfs(nums, target, i);//递归,注意这里是i(表示可以重复读取当前的数)

            list.remove(list.size() - 1);//回溯
            sum -= nums[i];
        }
    }

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates, target, 0);
        return res;

    }
}

复习题4

40. 组合总和 II - 力扣(LeetCode)

二叉搜索树

 本题较上题元素不能重复使用,并且数组中存在重复元素需要考虑去重的问题(利用set将选过的数进行保存,在遍历前判断是否存在来避免重复选择相同的元素,使用前提需要对数组进行排序,为了将相同元素放到一起)

回溯三步曲:

1.确认递归返回值及参数

    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();

res用于收集最后所有可能的结果,list用于存储符合条件的单一结果

2.递归终止条件

当list的元素的和为targetSum时既符合题目条件或者当sum大于targetSum时也没有继续搜索的必要了

  //sum>=target就返回
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.add(new ArrayList<>(list));
            return;
        }

3.单层搜索过程从startIndex进行搜索,将数组中的数加入,后向下递归回溯

  HashSet<Integer> set = new HashSet<>();
        for (int i = startIndex; i <nums.length ; i++) {
            if(set.contains(nums[i])){
                continue;
            }
            set.add(nums[i]);
            list.add(nums[i]);
            sum+=nums[i];

            dfs(nums,targetSum,sum,i+1);

            sum-=nums[i];
            list.remove(list.size()-1);
        }

 完整代码:

public static List<List<Integer>> res = new ArrayList<>();
    public static List<Integer> list = new ArrayList<>();

    /**

     */

    public static void dfs(int[]nums,int targetSum,int sum,int startIndex) {
         if(sum>targetSum){
                return;
          }
            if(sum==targetSum){
               res.add(new ArrayList<>(list));
               return;
            }
        HashSet<Integer> set = new HashSet<>();
        for (int i = startIndex; i <nums.length ; i++) {
            if(set.contains(nums[i])){
                continue;
            }
            set.add(nums[i]);
            list.add(nums[i]);
            sum+=nums[i];

            dfs(nums,targetSum,sum,i+1);

            sum-=nums[i];
            list.remove(list.size()-1);
        }
    }

    public static List<List<Integer>> f(int []nums, int targetSum) {
       Arrays.sort(nums);
       dfs(nums,targetSum,0,0);
       return res;
    }

复习题5

多个集合求组合

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

数字和字母如何映射

   利用map将数字和字符进行映射  


    public void f(String digits) {
        map.put(2, "abc");
        map.put(3, "def");
        map.put(4, "ghi");
        map.put(5, "jkl");
        map.put(6, "mno");
        map.put(7, "pqrs");
        map.put(8, "tuv");
        map.put(9, "wxyz");
    }

 回溯三部曲

1.确认参数和返回值

res用于存储最终答案,ans用于存储单一搜索答案

 public List<String> res = new ArrayList<>();

 public StringBuilder ans = new StringBuilder();

利用len表示当前res的长度,key表示当前遍历的数字

2.递归终止条件

如题可知当res的长度等于digits时递归终止

 if(len==digits.length()){//收集结果
            res.add(ans.toString());
            return;
        }
        

3.单一搜索过程

对digits进行遍历后对向下一个数字对应的字符进行递归

 
        String str=map.get(digits.charAt(key)-'0');//获取当前需要操作的字符串,如“23”,即先2,3
        
        for (int i =0; i <str.length() ; i++) {
              ans.append(str.charAt(i));//处理

              dfs(digits,map,len+1,key+1);//向下搜

              ans.delete(ans.length()-1,ans.length());//回溯

        }

完整代码:

class Solution {
    public HashMap<Integer, String> map = new HashMap<>();

    public List<String> res = new ArrayList<>();

    public StringBuilder ans = new StringBuilder();

    public void f(String digits) {
        map.put(2, "abc");
        map.put(3, "def");
        map.put(4, "ghi");
        map.put(5, "jkl");
        map.put(6, "mno");
        map.put(7, "pqrs");
        map.put(8, "tuv");
        map.put(9, "wxyz");
    }

     /**
     * 
     * @param digits  目标参数
     * @param map     映射map
     * @param num     当前操作的字符串在map中对应的key,
     * @param len     当前字符ans的长度
     */
    public  void dfs(String digits,HashMap<Integer,String> map,int len,int key ){
        f(digits);
        if(len==digits.length()){//收集结果
            res.add(ans.toString());
            return;
        }
        
        String str=map.get(digits.charAt(key)-'0');//获取当前需要操作的字符串,如“23”,即先2,3
        
        for (int i =0; i <str.length() ; i++) {
              ans.append(str.charAt(i));//处理

              dfs(digits,map,len+1,key+1);//向下搜

              ans.delete(ans.length()-1,ans.length());//回溯

        }
    }

    public List<String> letterCombinations(String digits) {
        if(digits==null||digits.length()==0){
            return res;
        }
        dfs(digits, map, 0,0);
        return res;
    }
}

复习题6

切割问题

131. 分割回文串 - 力扣(LeetCode)

 切割问题依然可以抽象成组合问题

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

判断回文

 public  boolean cheak(String s,int start,int end){
        for (int i = start,j=end; i <j ; i++,j--) {
            if(s.charAt(i)!=s.charAt(j)){
                return false;
            }
        }
        return true;
    }

 本题注意 利用startIndex代表分割线当startIndex大于了是s.length()也就到达了叶子节点、

完整代码:

class Solution {
       public List<List<String>>  res=new ArrayList<>();

    public  List<String> list=new ArrayList<>();

    public  boolean cheak(String s,int start,int end){
        for (int i = start,j=end; i <j ; i++,j--) {
            if(s.charAt(i)!=s.charAt(j)){
                return false;
            }
        }
        return true;
    }
    public  void dfs(String s,int startIndex) {
        if (startIndex >= s.length()) {
            res.add(new ArrayList(list));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            //如果是回文子串,则记录
            if (cheak(s, startIndex, i)) {
                String str = s.substring(startIndex, i + 1);
                list.add(str);
            } else {
                continue;
            }
            //起始位置后移,保证不重复
            dfs(s, i + 1);
            list.remove(list.size() - 1);
        }
    }
    public List<List<String>> partition(String s) {
        dfs(s, 0);
        return res;
    }
}

复习题7

子集问题

78. 子集 - 力扣(LeetCode)

子集问题与组合问题不同的是对所有的叶子节点都需要搜集,所以在搜集答案时不需要return 了

 

本题其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整棵树。

有的同学可能担心不写终止条件会不会无限递归?

并不会,因为每次递归的下一层就是从i+1开始的。

如果要写终止条件,注意:result.push_back(path);要放在终止条件的上面,如下:

result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉结果
if (startIndex >= nums.size()) { // 终止条件可以不加
    return;
}

 完整代码:

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

    public  List<Integer> list=new ArrayList<>();

    public  void dfs(int []nums,int startIndex){
        res.add(new ArrayList<>(list));
        for (int i = startIndex; i <nums.length ; i++) {
            list.add(nums[i]);
            dfs(nums,i+1);
            list.remove(list.size()-1);
        }
    }
    public List<List<Integer>> subsets(int[] nums) {
       dfs(nums,0);
       return res;
    }
}

回溯算法:求子集问题(二) (opens new window)中,开始针对子集问题进行去重。

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

    public  List<Integer> list=new ArrayList<>();

    public  void dfs(int []nums,int startIndex){
        res.add(new ArrayList<>(list));
        for (int i = startIndex; i <nums.length ; i++) {
            if(i>startIndex&&nums[i]==nums[i-1]){
                continue;
            }
            list.add(nums[i]);
            dfs(nums,i+1);
            list.remove(list.size()-1);
        }
    }
    public List<List<Integer>> subsetsWithDup(int[] nums) {
          Arrays.sort(nums);
        dfs(nums,0);
        return res;
    }
}

复习题8

排列问题

46. 全排列 - 力扣(LeetCode)

排列问题与祝贺问题不同的是排列问题与与顺序有关如[1,2]和[2,1]是相同的组合但是是不同的排列

那么可能前面选了2后面可能选1 也就说明每次都从0开始,然后需要used数组对已经选过的数进行保存

完整代码:

class Solution {
    public List<List<Integer>> res = new ArrayList<>();
    public List<Integer> list = new ArrayList<>();
    public boolean[] isUse = new boolean[10];

    /**
     * @param nums 数组
     * @param x    当前放数的位置
     */
    public  void dfs(int[] nums, int x) {
        if (x == nums.length) {
            res.add(new ArrayList<>(list));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (!isUse[i]) {

                isUse[i] = true;
                list.add(nums[i]);

                dfs(nums, x + 1);

                isUse[i] = false;
                list.remove(list.size() - 1);
            }
        }
    }

    public List<List<Integer>> permute(int[] nums) {
dfs(nums, 0);
        return res;
    }
}

47. 全排列 II - 力扣(LeetCode)

在此题基础上增加去重过程即可

class Solution {
      public  List<List<Integer>> res = new ArrayList<>();
    public  List<Integer> list = new ArrayList<>();
    public  boolean[] isUse = new boolean[10];

    /**
     * @param nums 数组
     * @param x    当前放数的位置
     */
    public void dfs(int[] nums, int x) {
        if (x == nums.length) {
            res.add(new ArrayList<>(list));
            return;
        }
        HashSet<Integer> set = new HashSet<>();
        for (int i = 0; i < nums.length; i++) {
            if(set.contains(nums[i])){//对同层选过的节点进行去重
                continue;
            }
            if (!isUse[i]) {

                isUse[i] = true;
                list.add(nums[i]);
                set.add(nums[i]);

                dfs(nums, x + 1);

                isUse[i] = false;
                list.remove(list.size() - 1);
            }
        }
    }
    public List<List<Integer>> permuteUnique(int[] nums) {
        dfs(nums, 0);
        return res;
    }
}

关于去重

去重问题

以上我都是统一使用used数组来去重的,其实使用set也可以用来去重!

本周小结!(回溯算法系列三)续集 (opens new window)中给出了子集、组合、排列问题使用set来去重的解法以及具体代码,并纠正一些同学的常见错误写法。

同时详细分析了 使用used数组去重 和 使用set去重 两种写法的性能差异:

使用set去重的版本相对于used数组的版本效率都要低很多,大家在leetcode上提交,能明显发现。

原因在回溯算法:递增子序列 (opens new window)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。

而使用used数组在时间复杂度上几乎没有额外负担!

使用set去重,不仅时间复杂度高了,空间复杂度也高了,在本周小结!(回溯算法系列三) (opens new window)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。

那有同学可能疑惑 用used数组也是占用O(n)的空间啊?

used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。

标签:res,nums,int,随想录,30,list,回溯,new,public
From: https://blog.csdn.net/weixin_74408291/article/details/139507433

相关文章

  • 60款柯达Kodak电影胶片调色预设SP-3000扫描仪配置文件2383电影卷2393视频lut优质精选
    60款柯达Kodak电影胶片调色预设SP-3000扫描仪配置文件2383电影卷2393视频lut优质精选预设预设在精不在多,素材湾倾心提供优质精选预设并整理安装使用教学助你学习工作中提升效率,更多时间专心于优质创作。*包含内容:5组共60款柯达电影胶片调色预设1款柯达SP-3000扫描仪配置(XMP......
  • python自动化脚本:12306-火车票购票
    1.导包:importtimefromseleniumimportwebdriverfromselenium.webdriver.common.byimportByfromselenium.webdriver.common.keysimportKeysfromselenium.webdriver.support.uiimportWebDriverWait2.选择浏览器驱动:这里选择的是Chromedriver=webdriver.Chrom......
  • 代码随想录算法训练营第三十天 | 51.N 皇后
    51.N皇后题目链接文章讲解视频讲解递归三部曲递归函数参数需要传入当前chessBoard和棋盘大小n,以及当前要放置皇后的行数rowvoidbacktracking(vector<string>&chessBoard,intn,introw);递归终止条件当最后一个皇后放置好后结束if(row==n){result.push_b......
  • Leetcode 300. 最长递增子序列
    https://leetcode.cn/problems/longest-increasing-subsequence/description/给你一个整数数组nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。示......
  • <PLC><工控>使用汇川PLC控制SV630N伺服驱动的循环运动程序(附PLC程序)
    前言本系列是关于PLC相关的博文,包括PLC编程、PLC与上位机通讯、PLC与下位驱动、仪器仪表等通讯、PLC指令解析等相关内容。PLC品牌包括但不限于西门子、三菱等国外品牌,汇川、信捷等国内品牌。除了PLC为主要内容外,PLC相关元器件如触摸屏(HMI)、交换机等工控产品,如果有值得记......
  • 【GD32F303红枫派使用手册】第九节 RTC-万年历实验
    9.1实验内容通过本实验主要学习以下内容:RTC简介RTC复位RTC实现万年历RTC使用注意事项9.2实验原理9.2.1RTC简介RTC(RealTimeClock)——实时时钟定时器,可以用作日历。RTC电路分两个电源域部分,其一位于备份域中,该部分包括一个32位的累加计数器、一个闹钟、一个预......
  • 代码随想录算法训练营第七天 | 四数之和、赎金信、三数之和、四数之和2
    代码随想录算法训练营第七天383赎金信https://leetcode.cn/problems/ransom-note/submissions/537782865/383赎金信代码随想录https://programmercarl.com/0383.赎金信.html#思路四数之和2https://leetcode.cn/problems/4sum-ii/四数之和2代码随想录https://programmerca......
  • 代码随想录算法训练营第八天 | 字符串:344反转字符串、
    反转字符串https://leetcode.cn/problems/reverse-string/反转字符串代码随想录https://programmercarl.com/0344.反转字符串.html#算法公开课反转字符串题目编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组s的形式给出。不要给另外的数组分配额外......
  • ICS TRIPLEX T8800C PD8800 PCB130100 数字输入模块
    T8800CPD8800PCB130100应用领域包括:化学工业纸张制造电力生成石油行业制造业电力行业化学行业等需要自动化控制的工业生产过程。T8800CPD8800PCB130100可以集成到自动化控制系统中,与其他设备和系统协同工作,以提高生产效率、降低能源消耗和减少劳动成本。它还可以设置每......
  • CF1730F Almost Sorted
    CF1730FAlmostSorted状压dp题目的描述有点奇怪,实际上就是将\(p\)在满足要求的情况下重排列,求下标的逆序对最小值。根据条件,我们猜测前面的数都不会很大,于是考虑从左到右插入值,若当前插入的值为\(a_i\),那么由限制条件可知,前面放的数都\(\lea_i+k\),同时\(\lea_i\)的部......