D23-回溯,leetcode39,40,131

  1. 组合总和
  • 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

  • candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

  • 对于给定的输入,保证和为 target 的不同组合数少于 150 个。

  • 思路

var combinationSum = function(candidates, target) {
    const res = [], path = [];
    candidates.sort((a,b)=>a-b); // 排序,方便剪枝
    backtracking(0, 0);
    return res;
    function backtracking(j, sum) {
        if (sum === target) {
            /* Array.from(path) 的作用是创建 path 的一个浅拷贝数组。
             在回溯算法中,path 是用来记录当前递归路径(即当前组合)的。
             
             由于 path 数组会不断 push 和 pop(回溯),如果直接 res.push(path),保存的是同一个引用,后续 path 变化会影响结果。
             
             用 Array.from(path) 或 path.slice() 可以拷贝当前 path 的内容,把当前组合的快照保存到结果数组中,后续 path 的变化不会影响已保存的结果。

            res.push([...path]);
            res.push(path.slice());
            res.push(Array.from(path));
            */
            res.push(Array.from(path));// 找到一个组合,加入结果
            return;
        }
        for(let i = j; i < candidates.length; i++ ) {
            const n = candidates[i];
            if(n > target - sum) break; // 剪枝:后面都比target大,直接退出
            path.push(n); // 选择当前数字
            sum += n;
            backtracking(i, sum); // 递归,i而不是i+1,允许重复选取当前数字
            path.pop(); // 回溯,撤销选择
            sum -= n;
        }
    }
};

  1. 组合总和 II
  • 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

  • candidates 中的每个数字在每个组合中只能使用 一次 。

  • 注意:解集不能包含重复的组合。

  • 思路

  • 数组排序,横向,树层去重,相邻元素相同,前面取过了,后面就不用取了,取了组成的组合也是前面组合过的,重复了

  • 定义一个数组used,用来记录同一树枝上的元素是否使用过,方便于集合去重

  • 如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。

/**
1. 排序:先排序,方便后续剪枝和去重。
2. 树层去重:if(j > i && candidates[j] === candidates[j-1]) continue;
  保证同一层(横向)不选重复数字,避免结果重复。
3. 剪枝:if(n > target - sum) break;
  当前数已经大于剩余目标值,后面更大,直接 break。
4. 递归参数:backtracking(sum, j + 1)
  每个数字只能用一次,所以递归下标是 j + 1。
5. 回溯:递归后撤销选择,恢复现场。
 */
var combinationSum2 = function(candidates, target) {
    const res = []; path = [], len = candidates.length;
    candidates.sort((a,b)=>a-b);// 排序,方便去重和剪枝
    backtracking(0, 0);
    return res;
    function backtracking(sum, i) {
        if (sum === target) {
            res.push(Array.from(path));// 找到一个组合,加入结果
            return;
        }
        for(let j = i; j < len; j++) {
            const n = candidates[j];
            // 树层去重:同一层遇到重复元素直接跳过,避免重复组合
            if(j > i && candidates[j] === candidates[j-1]){
              //若当前元素和前一个元素相等
              //则本次循环结束,防止出现重复组合
              continue;
            }
            //// 剪枝:当前元素大于剩余目标值,后面更大,直接退出
            //由于数组已排序,那么该元素之后的元素必定不满足条件
            //直接终止当前层的递归
            if(n > target - sum) break;
            path.push(n);// 选择当前数字
            sum += n;
            backtracking(sum, j + 1);// 递归到下一个位置(每个数只能用一次)
            path.pop();// 回溯,撤销选择
            sum -= n;
        }
    }
};
/**
1. 排序:保证相同数字相邻,便于去重和剪枝。
2. 树层去重:(i > 0 && cur === candidates[i - 1] && !used[i - 1])
  保证同一层(横向)不选重复数字,避免结果重复。
  只有当前一个同值元素在同一树枝未被使用时,才跳过当前元素。
3. 剪枝:cur > target - total,当前数已经大于剩余目标值,后面更大,直接跳过。
4. used 数组:用于区分树层和树枝,防止重复组合。在同一树层(横向)遇到重复元素时,判断前一个相同元素是否在本层被用过,从而避免同一层出现重复组合。
5. 回溯:递归后撤销选择,恢复现场。
 */
var combinationSum2 = function(candidates, target) {
    // res:存放所有满足条件的组合结果。
    // path:当前递归路径(正在拼接的组合)。
    // total:当前组合的和。
    // used:布尔数组,记录每个元素是否在当前递归路径中被使用过(用于树层去重)。
    // candidates.sort(...):先排序,方便后续剪枝和去重。
    let res = [];
    let path = [];
    let total = 0;
    const len = candidates.length;
    candidates.sort((a, b) => a - b);
    let used = new Array(len).fill(false);
    const backtracking = (startIndex) => {
        if (total === target) {
            res.push([...path]); // 找到一个组合,加入结果
            return;
        }
        for(let i = startIndex; i < len && total < target; i++) {
            const cur = candidates[i];
              // 剪枝:当前数大于剩余目标值,直接跳过
            // 树层去重:同一层遇到重复元素且前一个没用过,跳过
            if (cur > target - total || (i > 0 && cur === candidates[i - 1] && !used[i - 1])) continue;
            path.push(cur);
            total += cur;
            used[i] = true;
            // 每个数只能用一次,递归下标是 i+1
            backtracking(i + 1);
            path.pop();
            total -= cur;
            used[i] = false;
        }
    }
    backtracking(0);
    return res;
};

  1. 分割回文串
  • 给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

  • 思路

  • 判断一个字符串是否是回文。

  • 可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。

// 判断是否是回文串,双指针,前后一起移动
const isPalindrome = (s, l, r) => {
    // 用双指针从两端向中间比较字符,只要有一对不相等就返回 false。
    for (let i = l, j = r; i < j; i++, j--) {
        if(s[i] !== s[j]) return false;
    }
    return true;
}

var partition = function(s) {
    // res:存放所有分割方案。
    // path:当前递归路径(正在拼接的分割子串)。
    // backtracking(startIndex):从 startIndex 开始,尝试所有可能的分割。
    const res = [], path = [], len = s.length;
    backtracking(0);
    return res;
    /**回溯过程
    1. 终止条件
    如果 startIndex >= len,说明已经分割到字符串末尾,把当前 path 加入结果。
    2. 枚举分割点
    枚举区间 [startIndex, i],判断 s[startIndex...i] 是否为回文串。
    如果是回文串,就把这个子串加入 path,递归处理剩下的部分。
    回溯时撤销选择(path.pop())
     */
    function backtracking(startIndex) {
        if(startIndex >= len) {
            res.push(Array.from(path));
            return;
        }
        for(let i = startIndex; i < len; i++) {
            if(!isPalindrome(s, startIndex, i)) continue;
            path.push(s.slice(startIndex, i + 1));
            backtracking(i + 1);
            path.pop();
        }
    }
};



参考&感谢各路大神

posted @ 2025-06-19 12:26  安静的嘶吼  阅读(7)  评论(0)    收藏  举报