D24-回溯,leetcode93,78,90
- 复原 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()// 回溯,撤销选择
}
}
};
- 子集
-
给你一个整数数组 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
};
- 子集 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 的变化在递归调用时体现,每次递归都向后推进,确保组合的唯一性和不重复。
*/
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。