D24-回溯,leetcode93,78,90

  1. 复原 IP 地址
  • 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

  • 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

  • 思路

  • 切割问题就可以使用回溯搜索法把所有可能性搜出来

/**
path:存储当前分割出来的每一段。
len > 4:如果已经分了超过4段,直接返回(剪枝)。
len === 4 && i === s.length:正好分成4段且用完所有字符,说明是一个有效 IP,加入结果。
for 循环:尝试分割出长度为1~3的每一段。
str.length > 3 || +str > 255:如果当前段大于255或长度大于3,直接 break(后面更长只会更大)。
str.length > 1 && str[0] === "0":有前导0的段无效,直接 break。
递归:每次递归从下一个字符开始,继续分割下一段。
 */
var restoreIpAddresses = function(s) {
    const res = [], path = [];
    backtracking(0, 0)
    return res;
    function backtracking(i) {
        const len = path.length;
        if(len > 4) return;// 超过4段直接剪枝
        if(len === 4 && i === s.length) {
            res.push(path.join("."));// 恰好4段且用完所有字符,加入结果
            return;
        }
        for(let j = i; j < s.length; j++) {
            const str = s.slice(i, j + 1);// 取出当前段
            if(str.length > 3 || +str > 255) break;// 段大于255或长度大于3直接剪枝
            if(str.length > 1 && str[0] === "0") break;// 有前导0直接剪枝
            path.push(str);// 选择当前段
            backtracking(j + 1);// 递归处理下一段
            path.pop()// 回溯,撤销选择
        }
    }
};

  1. 子集
  • 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

  • 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

  • 思路

  • 组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点

  • 全局变量数组path为子集收集元素,二维数组result存放子集组合

/**
 * result:存放所有子集的二维数组。
path:当前递归路径,表示一个子集。
backtracking(startIndex):从 startIndex 开始,依次尝试选取每个元素。
每次递归都把当前 path 加入 result,收集所有节点(不是只收集叶子节点)。
for 循环控制树的横向遍历,递归控制树的纵向遍历。

回溯法枚举所有子集,每到一个节点就收集一次当前组合,最终得到所有可能的子集。
 */
var subsets = function(nums) {
    let result = []
    let path = []
    function backtracking(startIndex) {
        // 每次递归都先收集当前 path(即所有节点都收集)。
        result.push([...path])// 每到一个节点都收集一次当前子集
        if(startIndex > nums.length) return   // 终止条件可以省略,因为 for 循环自然会终止
        for(let i = startIndex; i < nums.length; i++) {
            path.push(nums[i])// 选择当前元素
            backtracking(i + 1)// 递归,下一层从下一个元素开始
            path.pop() // 回溯,撤销选择
        }
    }
    backtracking(0)
    return result
};

  1. 子集 II
    – 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
    – 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
  • 思路
/**
1. 排序
  先对 nums 排序,使得相同元素相邻,便于后续去重。
2. 去重逻辑
  if(i > startIndex && nums[i] === nums[i - 1]) continue;
  只要在同一树层遇到重复元素,只选第一个,后面的跳过,避免重复子集。
3. 回溯过程
  每到一个节点都收集一次当前 path,保证所有子集都被收集。
  递归时 i + 1,表示每个元素只能选一次。
4. 终止条件
  if(startIndex > nums.length - 1) return; 其实可以省略,因为 for 循环自然会终止。
 */
var subsetsWithDup = function(nums) {
    let result = []
    let path = []
    // 先排序,方便去重
    let sortNums = nums.sort((a, b) => {
        return a - b
    })
    function backtracing(startIndex, sortNums) {
        // 每到一个节点都收集一次当前子集
        result.push([...path])
        if(startIndex > nums.length - 1) {
            return
        }
        for(let i = startIndex; i < nums.length; i++) {
             // 树层去重:同一层遇到重复元素只选第一个
            if(i > startIndex && nums[i] === nums[i - 1]) {
                continue
            }
            path.push(nums[i])
            backtracing(i + 1, sortNums)
            path.pop()
        }
    }
    backtracing(0, sortNums)
    return result
};
/**
每次递归调用 backtracing(i + 1, sortNums),startIndex 就变成了 i + 1,也就是每次递归都从下一个元素开始遍历,不会重复选择当前元素及其之前的元素。
 
第一层 startIndex = 0,for 循环 i 从 0 开始
递归到下一层时,backtracing(i + 1, sortNums),所以下一层的 startIndex 就是 i + 1
这样保证了每个元素只会被选一次,且组合不会重复

startIndex 的变化在递归调用时体现,每次递归都向后推进,确保组合的唯一性和不重复。
 */



参考&感谢各路大神

posted @ 2025-06-20 11:25  安静的嘶吼  阅读(9)  评论(0)    收藏  举报