回溯算法可以形象地理解为在一棵n 叉树上的探索过程,其核心机制就是"开枝散叶"与"修剪枝条"的有机结合
理解回溯:以 Leetcode 93 题"复原 IP 地址"为例:
🌿 开枝过程
- 每个 IP 段的长度范围为 [1, 3],因此每层循环最多扩展3 个分支
这相当于在树的每个节点处,横向展开1-3 条可能的路径
🍃 散叶过程
- 选中一个有效分支后,纵向深入递归探索
- IP 地址需要 4 个段(被 3 个"."分割),因此需要递归 4 层
- 这个过程如同枝叶从主干不断向外蔓延生长
剪枝:智慧的"修剪艺术"
回溯中的剪枝,本质上是"砍掉 n 叉树中无效枝条"的优化策略。面对大规模问题和深层递归时,及时识别并剪除不可能的分支至关重要
剪枝的四重境界
- 整树剪枝:问题本身无解,根本不开始搜索
- 子树剪枝:当前节点的所有子孙都不可能,整体放弃
- 分支剪枝:当前分支的所有延伸都无效,终止该分支
- 路径剪枝:当前路径已违反约束,立即回溯
剪枝的哲学
剪枝如同睿智的园丁:在枝条初生时便预判其未来——若不能开花结果,就及时剪除,避免浪费宝贵的养分(计算资源)。
这种"开枝散叶寻可能,修剪枝条保效率"的思维,正是回溯算法的精髓所在
举个例子
版本1:
var restoreIpAddresses = function(s) {
let length = s.length, result = [], path = []
// 判断字符串 string 在范围 [startIndex, endIndex] 区间内的字符串是否是有效的 IP 地址段
const isValidate = (string, startIndex, endIndex) => {
let ipTextLength = endIndex - startIndex + 1
/*
非法 IP 地址段的特点:
1. 长度大于3
2. 如果首位是 '1',且长度不为1
3. 转换为数字后,不在 [0, 255] 区间
*/
if (ipTextLength > 3) return false
if (string[startIndex] === '0' && ipTextLength !== 1) return false
let text = string.substring(startIndex, endIndex + 1)
if (Number(text) > 255 || Number(text) < 0) return false
return true
}
const backtrace = (startIndex) => {
// 收集结果
if (path.length >= 4 || startIndex >= length ) {
if (startIndex === length && path.length === 4) result.push(path.join('.'))
return
}
for (let i = startIndex; i < length; i++) {
if (isValidate(s, startIndex, i) === true) {
path.push(s.substring(startIndex, i + 1))
backtrace(i + 1)
path.pop()
}
}
}
backtrace(0)
return result
};版本2:
var restoreIpAddresses = function(s) {
let result = []
// 第1点:题目中只说明 s 长度为:1 <= s.length <= 20,并不一定符合有效 IP 段。所以要做检测
if (s.length < 4 || s.length > 12) return result
const isValidate = (testString, startIndex, endIndex) => {
//1. 非法的情况:转换为数字,不在 [0, 255] 区间内;2. 长度大于1的情况下,首位是0;
const length = endIndex - startIndex + 1
if (length > 1 && testString[startIndex] === '0') return false
const num = testString.substring(startIndex, endIndex + 1)
if (Number(num) > 255 || Number(num) < 0) return false
return true
}
const backtracking = (path, startIndex) => {
// 第2点:递归结束条件,收集结果
if (startIndex >= s.length) {
if (path.length === 4) {
result.push(path.join('.'))
}
return
}
let needSegmentsCount = 4 - path.length
let remainCharCount = s.length - startIndex
/*
条件1:要构成一个有效 IP 的字符串个数 大于 剩余的字符串个数,则非法,不可能构成有效 IP。有效的 IP 至少1个字符,所以 needSegmentsCount < remainCharCount
条件2:如果现有3个 IP 段了,且总长度为 currentCount。接下去需要寻找到一个合法的 IP 段,那么合格的 IP 段最长为3.
所以:
所需要的段个数为 needSegmentsCount。其中每个有效的 IP 段,长度为 [1, 3]
剩余字符串个数为:remainCharCount
所以:
- 剩余字符串过少不行:needSegmentsCount * 1 > remainCharCount
- 剩余字符串过多也不行:remainCharCount > needSegmentsCount * 3
*/
// 第3点:
if (remainCharCount < needSegmentsCount || remainCharCount > needSegmentsCount*3) {
return
}
// 第4点:避免 for 多次重复计算
let upperBound = Math.min(startIndex + 3, s.length)
for (let i = startIndex; i < upperBound; i++) {
let IPSegmentTestString = s.substring(startIndex, i + 1)
if (Number(IPSegmentTestString) < 0 || Number(IPSegmentTestString) > 255) {
break
}
if (IPSegmentTestString.length > 1 && IPSegmentTestString[0] === '0') {
break
}
path.push(IPSegmentTestString)
backtracking(path, i + 1)
path.pop()
}
}
backtracking([], 0)
return result
};可以看到正确的剪枝,时间复杂度、空间复杂度大大提高了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。