day23 代码随想录算法训练营 回溯专题2

1 今日打卡

组合总和 39. 组合总和 - 力扣(LeetCode)

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

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

2 组合总和

2.1 思路

剪枝:sum > target 时直接返回,减少无效递归,提升效率。

终止条件:sum == target 时,必须创建 path 的新副本存入 res(因为 path 是全局变量,后续回溯会修改它)。

循环与递归:

startIndex:确保组合不重复(比如选了 2 之后,只从 2 及之后的元素选,避免出现 2,33,2 这种重复组合)。

递归时传入 i 而非 i+1:允许重复选取当前元素(比如 candidates=2,3,6,7,target=7 时,能选 2,2,3)。

回溯:递归返回后,撤销当前选择(移除元素、恢复 sum),继续尝试下一个元素。

2.2 实现代码

java 复制代码
class Solution {
    // 全局变量:存储当前正在构建的单个组合(路径),如[2,2]
    List<Integer> path = new ArrayList<>();
    // 全局变量:存储最终所有符合条件的组合(结果集),如[[2,2,3],[7]]
    List<List<Integer>> res = new ArrayList<>();
    // 全局变量:实时记录当前path中元素的和,避免每次遍历path求和,提升效率
    int sum = 0;

    /**
     * 主方法:对外暴露的入口,调用回溯函数并返回最终结果
     * @param candidates 无重复元素的候选数字数组
     * @param target 目标和
     * @return 所有和为target的组合列表
     */
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 调用回溯核心函数,起始索引为0(控制组合不重复的关键)
        backtracking(candidates, target, 0);
        return res;
    }

    /**
     * 回溯核心函数:递归遍历所有可能的组合,实现"选择-递归-回溯"的核心逻辑
     * @param candidates 候选数字数组
     * @param target 目标和
     * @param startIndex 遍历起始索引(避免组合重复,如[2,3]和[3,2])
     */
    public void backtracking(int[] candidates, int target, int startIndex) {
        // 剪枝条件:当前路径和超过目标值,无需继续递归,直接返回(减少无效递归)
        if (sum > target) {
            return;
        }

        // 终止条件:当前路径和等于目标值,找到有效组合
        if (sum == target) {
            // 必须创建path的副本存入res!因为path是全局变量,后续回溯会修改它
            res.add(new ArrayList<>(path));
            return; // 找到结果后返回上一层递归,继续寻找其他组合
        }

        // 单层递归逻辑:横向遍历当前层的所有可选数字
        // i从startIndex开始,保证组合不重复(只向后选,不回头选)
        for (int i = startIndex; i < candidates.length; i++) {
            // 1. 选择当前元素:将数字加入路径,更新当前和
            path.add(candidates[i]);
            sum += candidates[i];

            // 2. 递归深入:继续选择下一个数字(传i而非i+1,允许重复选取当前元素)
            backtracking(candidates, target, i);

            // 3. 回溯撤销:移除最后一个元素,恢复sum,准备尝试下一个数字
            sum -= candidates[i];
            path.remove(path.size() - 1); // 栈式移除,只删最后一个元素
        }
    }

3 组合总和Ⅱ

3.1 思路

排序的作用:

让重复元素相邻,为 "同一层跳过重复元素" 的去重逻辑铺路;

让数组有序,可在循环中通过 sum + candidatesi > target 提前终止循环(剪枝)。

终止条件和剪枝:

sum > target:剪枝条件,提前终止无效递归(比如 sum=8、target=7 时,直接返回);

sum == target:终止条件,找到符合条件的组合,必须创建 path 的新副本存入 res(因为 path 是全局变量,后续回溯会修改它)。

去重逻辑:

去重优化:排序后,同一层递归中跳过重复元素(i > startIndex && candidatesi==candidatesi-1),避免结果出现重复组合;

3.2 实现代码

java 复制代码
class Solution {
    // 存储当前正在构建的单个组合(路径)
    List<Integer> path = new ArrayList<>();
    // 存储最终所有符合条件的组合(结果集)
    List<List<Integer>> res = new ArrayList<>();
    // 实时记录当前path中元素的和(优化效率,避免重复求和)
    int sum = 0;

    /**
     * 主方法:对外暴露的入口,处理排序并调用回溯函数
     * @param candidates 包含重复元素的候选数组
     * @param target 目标和
     * @return 所有符合条件的组合列表
     */
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 排序:1.让重复元素相邻(为去重铺路);2.支持提前剪枝
        Arrays.sort(candidates);
        // 调用回溯函数,起始索引为0(控制遍历起点,避免组合顺序重复)
        backtracking(candidates, target, 0);
        return res;
    }

    /**
     * 回溯核心函数:递归遍历所有可能的组合,实现"选-递归-回溯"逻辑
     * @param candidates 候选数组(已排序)
     * @param target 目标和
     * @param startIndex 遍历起始索引(控制组合不重复,且元素仅用一次)
     */
    public void backtracking(int[] candidates, int target, int startIndex) {
        // 剪枝条件:当前和超过目标值,无需继续递归,直接返回
        if (sum > target) {
            return;
        }

        // 终止条件:找到和为target的有效组合
        if (target == sum) {
            // 必须创建path的副本存入res!否则回溯会修改res中的内容
            res.add(new ArrayList<>(path));
            return;
        }

        // 单层递归逻辑:横向遍历当前层的所有可选元素
        for (int i = startIndex; i < candidates.length; i++) {
            // 优化剪枝:排序后,当前元素+sum > target → 后续元素更大,直接终止循环
            if (sum + candidates[i] > target) {
                break;
            }

            // 去重核心逻辑:同一层递归中,跳过和前一个元素重复的元素
            // i > startIndex:保证是"同一层"的重复元素(而非不同层)
            if (i > startIndex && candidates[i] == candidates[i - 1]) {
                continue;
            }

            // 1. 选择当前元素:加入路径,更新当前和
            path.add(candidates[i]);
            sum += candidates[i];

            // 2. 递归深入:i+1 表示当前元素只能用一次(和combinationSum1的核心区别)
            backtracking(candidates, target, i + 1);

            // 3. 回溯撤销:移除最后一个元素,恢复sum,准备选下一个元素
            sum -= candidates[i];
            path.remove(path.size() - 1);
        }
    }

4 分割回文串

4.1 思路

循环变量 i:表示「当前切割的结束位置」,从 startIndex 开始遍历(比如 startIndex=0 时,i 依次取 0、1、2...,对应切割出 s0、s0-1、s0-2...);

isPalind(str, startIndex, i):判断 startIndex, i 区间的子串是否为回文(只有是回文才允许切割);

str.substring(startIndex, i + 1):截取 startIndex, i 区间的子串(因为 substring 是左闭右开,所以结束索引要 + 1);

递归传 i + 1:下一次切割从当前结束位置的下一个字符开始(保证子串不重叠、不遗漏);

回溯:递归返回后,移除 path 最后一个元素,回到切割前的状态,尝试下一个切割点。

4.2 实现代码

java 复制代码
class Solution {
    // 存储当前正在构建的分割方案(如["a","a"])
    List<String> path = new ArrayList<>();
    // 存储所有符合条件的分割方案
    List<List<String>> res = new ArrayList<>();

    /**
     * 主方法:对外暴露的入口,调用回溯函数并返回结果
     * @param s 待分割的字符串
     * @return 所有符合条件的分割方案
     */
    public List<List<String>> partition(String s) {
        // 调用回溯函数,起始切割位置为0(从第一个字符开始)
        backtracking(s, 0);
        return res;
    }

    /**
     * 回溯核心函数:递归切割字符串,寻找所有回文分割方案
     * @param str 待分割的原字符串
     * @param startIndex 下一次切割的起始位置(控制切割不重复、不遗漏)
     */
    public void backtracking(String str, int startIndex) {
        // 终止条件:切割完整个字符串(startIndex超出字符串长度)
        if (startIndex >= str.length()) {
            // 存入path的副本!避免回溯修改res中的内容
            res.add(new ArrayList<>(path));
            return;
        }

        // 单层递归逻辑:遍历所有可能的切割结束位置
        // i表示当前切割的结束位置(从startIndex开始,到字符串末尾)
        for (int i = startIndex; i < str.length(); i++) {
            // 关键判断:[startIndex, i]区间的子串是否为回文
            if (isPalind(str, startIndex, i)) {
                // 是回文:截取该子串(substring左闭右开,所以结束索引+1)
                String palindromeSub = str.substring(startIndex, i + 1);
                // 将该回文子串加入当前分割方案
                path.add(palindromeSub);
                // 递归:下一次切割从i+1开始(当前子串已处理完,不重复切割)
                backtracking(str, i + 1);
                // 回溯:撤销本次切割,尝试下一个切割点
                path.remove(path.size() - 1);
            }
            // 不是回文:直接跳过,不切割该区间
        }
    }

    /**
     * 辅助方法:双指针法判断指定区间的子串是否为回文
     * @param str 原字符串
     * @param left 子串左边界(包含)
     * @param right 子串右边界(包含)
     * @return true=是回文,false=不是回文
     */
    public boolean isPalind(String str, int left, int right) {
        // 双指针向中间靠拢,逐一比较字符
        while (left <= right) {
            // 字符不相等,直接返回false
            if (str.charAt(left) != str.charAt(right)) {
                return false;
            }
            left++;  // 左指针右移
            right--; // 右指针左移
        }
        // 所有字符都相等,是回文
        return true;
    }
相关推荐
小欣加油4 小时前
leetcode56 合并区间
c++·算法·leetcode·职场和发展
lqqjuly4 小时前
前沿算法深度解析(二)
人工智能·算法·机器学习
徐小夕5 小时前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
akunkuntaimei6 小时前
2026年高考数学各省真题及答案(完整版)
算法·高考
Hello:CodeWorld6 小时前
C 风格变参 vs C++ 变参模板:核心区别与选型指南
c语言·c++·算法
8Qi87 小时前
LeetCode 516:最长回文子序列
算法·leetcode·职场和发展·动态规划
youngerwang9 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
KaMeidebaby9 小时前
卡梅德生物技术快报|纯化重组蛋白实操详解
人工智能·python·tcp/ip·算法·机器学习
手写码匠10 小时前
从零实现 Prompt 工程引擎:结构化提示、自动优化与多轮自省体系
人工智能·深度学习·算法·aigc
无限码力10 小时前
阿里算法岗 0530笔试真题 - 多约束条件下的元素匹配统计
算法·阿里笔试真题·阿里机试真题·阿里算法岗笔试