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

1 今日打卡

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

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

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

2 组合总和

2.1 思路

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

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

循环与递归:

startIndex:确保组合不重复(比如选了 2 之后,只从 2 及之后的元素选,避免出现 [2,3] 和 [3,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 + candidates[i] > target 提前终止循环(剪枝)。

终止条件和剪枝:

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

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

去重逻辑:

去重优化:排序后,同一层递归中跳过重复元素(i > startIndex && candidates[i]==candidates[i-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...,对应切割出 s[0]、s[0-1]、s[0-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;
    }
相关推荐
Wect4 小时前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
NAGNIP16 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
颜酱1 天前
单调栈:从模板到实战
javascript·后端·算法
CoovallyAIHub1 天前
仿生学突破:SILD模型如何让无人机在电力线迷宫中发现“隐形威胁”
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
从春晚机器人到零样本革命:YOLO26-Pose姿态估计实战指南
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
Le-DETR:省80%预训练数据,这个实时检测Transformer刷新SOTA|Georgia Tech & 北交大
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
强化学习凭什么比监督学习更聪明?RL的“聪明”并非来自算法,而是因为它学会了“挑食”
深度学习·算法·计算机视觉
CoovallyAIHub1 天前
YOLO-IOD深度解析:打破实时增量目标检测的三重知识冲突
深度学习·算法·计算机视觉
NAGNIP2 天前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP2 天前
一文搞懂激活函数!
算法·面试