2
头图

回溯算法可以形象地理解为在一棵n 叉树上的探索过程,其核心机制就是"开枝散叶""修剪枝条"的有机结合

理解回溯:以 Leetcode 93 题"复原 IP 地址"为例:

🌿 开枝过程

  • 每个 IP 段的长度范围为 [1, 3],因此每层循环最多扩展3 个分支
  • 这相当于在树的每个节点处,横向展开1-3 条可能的路径

    🍃 散叶过程

  • 选中一个有效分支后,纵向深入递归探索
  • IP 地址需要 4 个段(被 3 个"."分割),因此需要递归 4 层
  • 这个过程如同枝叶从主干不断向外蔓延生长

剪枝:智慧的"修剪艺术"

回溯中的剪枝,本质上是"砍掉 n 叉树中无效枝条"的优化策略。面对大规模问题和深层递归时,及时识别并剪除不可能的分支至关重要

剪枝的四重境界

  1. 整树剪枝:问题本身无解,根本不开始搜索
  2. 子树剪枝:当前节点的所有子孙都不可能,整体放弃
  3. 分支剪枝:当前分支的所有延伸都无效,终止该分支
  4. 路径剪枝:当前路径已违反约束,立即回溯

剪枝的哲学

剪枝如同睿智的园丁:在枝条初生时便预判其未来——若不能开花结果,就及时剪除,避免浪费宝贵的养分(计算资源)。
这种"开枝散叶寻可能,修剪枝条保效率"的思维,正是回溯算法的精髓所在

举个例子

版本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
    };

可以看到正确的剪枝,时间复杂度、空间复杂度大大提高了


杭城小刘
1.2k 声望5.1k 粉丝

iOS - EntjA - 95后 - 养了3只布偶猫