【算法笔记】有人看海,有人被爱,有人做不出 leetcode 第一题(回溯专题 Part 1)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


Day 19: 回溯算法


回溯算法理论基础


文章讲解:代码随想录

视频讲解:带你学透回溯算法(理论篇)| 回溯法精讲!

题目建议:其实在讲解二叉树的时候,就给大家介绍过回溯,这次正式开启回溯算法,大家可以先看视频,对回溯算法有一个整体的了解。


回溯算法大纲

回溯模板如下:

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

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

组合


题目链接:77. 组合 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!

剪枝操作:带你学透回溯算法-组合问题的剪枝操作(对应力扣题目:77.组合)| 回溯法精讲

题目建议:对着 在 回溯算法理论基础 给出的 代码模板,来做本题组合问题,大家就会发现 写回溯算法套路。在回溯算法解决实际问题的过程中,大家会有各种疑问,先看视频介绍,基本可以解决大家的疑惑。本题关于剪枝操作是大家要理解的重点,因为后面很多回溯算法解决的题目,都是这个剪枝套路


image-20250519100205867image-20250519100217635image-20250519100230772

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        
    }
}

题目解析


核心思路

  • 使用回溯算法递归搜索所有可能的组合77.组合
  • 通过 startIndex 参数避免重复组合77.组合2
  • 到叶子节点时,就拿到了结果集合之一77.组合3
  • 可以进行剪枝优化减少不必要的搜索

方法一:基础实现(未剪枝)

回溯框架:

  • 使用递归代替多层嵌套循环
  • 通过 path 记录当前组合
  • 达到长度 k 时保存结果
class Solution {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int n, k;

    public List<List<Integer>> combine(int n1, int k1) {
        n = n1;
        k = k1;
        dfs(1);
        return ret;
    }

    private void dfs(int startIndex) {
        if (path.size() == k) {
            ret.add(new ArrayList<>(path));  
            // err: 不能直接 add(path), 而是新创建一个以当前 path 为副本的 List 再 add
            return;
        }

        for (int i = startIndex; i <= n; i++) {
            path.add(i);
            dfs(i + 1);  // err: 易错点, 不是 dfs(startIndex + 1)
            path.remove(path.size() - 1);  // 恢复现场
        }
    }
}

方法二:剪枝优化实现

剪枝优化:

  • 计算剩余需要的元素数量:k - path.size()
  • 调整循环上限:n - (k - path.size()) + 1
  • 避免无效搜索,提高效率

77.组合4


class Solution {
    List<List<Integer>> ret = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int n, k;
    
    public List<List<Integer>> combine(int n1, int k1) {
        n = n1; k =k1;
        dfs(1);
        return ret;
    }
    
    private void dfs(int start) {
        if (path.size() == k) {
            ret.add(new ArrayList<>(path));
            return;
        }
        
        // 剪枝优化:i <= n - (k - path.size()) + 1
        // 比如是1-10,3
        // 现在有1个了,还需要两个 k - path.size(), 那么i不能大于9
        // 因为 i > 9, path 最多只有 (9, 10),达不到 k 个元素, 所以去掉这种情况 
        // 10 - 2 + 1 = 9     n - k + path.size + 1
        for (int i = start; i <= n - (k - path.size()) + 1; i++) {
            path.add(i);
            dfs(i + 1);
            path.removeLast(); // 回溯
        }
    }
}

  1. 时间复杂度:
    • 优化前:O(C(n,k) × k)
    • 优化后:最坏情况相同,但实际运行更快
  2. 空间复杂度:
    • O(k) 递归调用栈深度
    • O(k) 临时 path 存储

这种解法是组合问题的经典回溯解决方案,通过剪枝优化可以显著提高性能。


组合总和 Ⅲ


题目链接:216. 组合总和 III - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III

题目建议:如果把 组合问题理解了,本题就容易一些了。


image-20250519100006933image-20250519100020844image-20250519100032091image-20250519100045167image-20250519100104604

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        
    }
}

题目解析


  1. 问题描述:在集合 [1, 2, 3, 4, 5, 6, 7, 8, 9] 中找到和为 nk 个数的组合。
  2. 77. 组合 的关系:本题是 [77. 组合] 的变种,多了一个限制条件,即组合的和为 n
  3. 集合固定:集合是 [1, 2, 3, 4, 5, 6, 7, 8, 9],共有 9 个数。
  4. 树的深度和宽度:
    • k 是组合的大小(树的深度)。
    • 集合的大小是 9(树的宽度)。
  5. 示例:对于 k = 2n = 4,需要在集合中找到和为 4 的 2 个数的组合。选取过程如图:

216.组合总和III


核心思路

  • 使用回溯算法遍历所有可能的组合
  • 通过剪枝优化减少不必要的搜索
  • 当组合长度等于 k 且和等于 n 时记录结果

代码实现

class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> ret = new ArrayList<>();
    int k, n;
    public List<List<Integer>> combinationSum3(int k1, int n1) {
        k = k1; n = n1;
        dfs(1, 0);
        return ret;
    }

    private void dfs(int start, int sum){
        if(sum > n) return; // err: 提前停止递归
            
        if(path.size() == k && sum == n){
            ret.add(new ArrayList<>(path));
            return;
        }

        for(int i = start; i <= 9 ; i++){  // err: 题目说的是只能使用 1-9, 而不是 1-n
            path.add(i);
            sum += i;
            dfs(i + 1, sum);
            path.remove(path.size()-1);
            sum -= i ;
        }
    }
}

复杂度分析

  • 时间复杂度:O(C(9,k)) = O(9^k),实际通过剪枝会更快
  • 空间复杂度:O(k),递归栈深度和临时路径存储

关键点总结

  1. 回溯框架:递归尝试每个可能的数字,满足条件时记录结果
  2. 剪枝优化:
    • 当当前和超过目标值时提前终止
    • 限制循环范围确保剩余数字足够完成组合
  3. 回溯操作:在递归返回时需要撤销上一步的选择

这个解法高效地遍历了所有可能的组合,同时通过剪枝避免了不必要的搜索,是解决此类组合问题的经典方法。


电话号码的字母组合


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

文章讲解:代码随想录

视频讲解:还得用回溯算法!| LeetCode:17.电话号码的字母组合

题目建议:本题大家刚开始做会有点难度,先自己思考20min,没思路就直接看题解。


image-20250519101115043image-20250519101128286image-20250519101144089

class Solution {
    public List<String> letterCombinations(String digits) {
        
    }
}

题目解析


  1. 回溯法解决多层循环问题:

    • 输入如“23”时,可以抽象为树形结构树的深度等于输入字符串的长度叶子节点是需要收集的结果17. 电话号码的字母组合
    • 回溯法通过递归实现动态的多层循环。

  1. 回溯三部曲:

    • 确定回溯函数参数:
      • 使用字符串数组ret保存结果,使用字符串s收集当前路径。
      • 参数包括输入的数字字符串digits和当前处理的索引index
    • 确定终止条件:
      • index等于digits.length()时,说明已经处理完所有数字,将当前路径s加入结果列表result
    • 确定单层遍历逻辑:
      • 根据digits[index]找到对应的字母集合。
      • 使用for循环遍历字母集合,递归处理下一层(index + 1),并在递归返回后回溯。

  1. 数字和字母的映射:

    • 使用数组 hash 存储数字到字母的映射关系。

      String[] hash = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
      List<String> ret = new ArrayList<>();
      StringBuilder path = new StringBuilder();
      

  1. 异常情况处理:

    • 输入可能包含无效字符(如1*#等),需要在代码中考虑这些情况。

  1. 代码实现:

    class Solution {
        String[] hash = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
        List<String> ret = new ArrayList<>();
        StringBuilder path = new StringBuilder();
    
        public List<String> letterCombinations(String digits) {
            if (digits.length() == 0) return ret;  // err: 边界条件是 digits.length() == 0, 其他都不对
            dfs(digits, 0);
            return ret;
        }
    
        private void dfs(String digits, int index) {
            if (path.length() == digits.length()) {
                ret.add(path.toString());
                return;
            }
    
            String cur = hash[digits.charAt(index) - '0'];
    
            for (int i = 0; i < cur.length(); i++) {
    
                path.append(cur.charAt(i));
                
                // 拼接好一个 hash 元素后, 又去拼接另一个 hash 元素
                dfs(digits, index + 1);
                
                // 注意 StringBuilder 删除字符的 API
                path.deleteCharAt(path.length() - 1);
            }
        }
    }
    

  1. 时间复杂度和空间复杂度:

    • 时间复杂度:O(3^m * 4^n),其中m是对应三个字母的数字个数,n是对应四个字母的数字个数。
    • 空间复杂度:O(3^m * 4^n)

  1. 总结:

    • 本题是多个集合求组合的问题,与组合问题(如77.组合216.组合总和III)的区别在于每个数字对应不同的集合。
    • 回溯法是解决此类问题的有效方法,关键在于理解递归和回溯的逻辑。

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值