回溯算法专题(八):精细化切割——还原合法的「IP 地址」

哈喽各位,我是前端小L。

欢迎来到我们的回溯算法专题第八篇!IP 地址我们都很熟悉,比如 192.168.1.1。 它有以下铁律:

  1. 段数固定 :必须正好分成 4 段

  2. 数值范围 :每段是 0255 之间的整数。

  3. 前导零限制 :除了单独的 0 以外,不能出现以 0 开头的数字(比如 01, 00 是非法的,但 0 是合法的)。

给定一个只包含数字的字符串,我们要把它还原成所有可能的 IP 地址。这本质上还是一个切割问题,但我们需要更小心地控制刀法。

力扣 93. 复原 IP 地址

https://leetcode.cn/problems/restore-ip-addresses/

题目分析:

  • 输入 :字符串 s(纯数字)。

  • 目标 :插入三个 .,将其变成一个合法的 IP。

  • 输出:所有可能的 IP 字符串列表。

例子: s = "25525511135"

  • 255.255.11.135 (合法)

  • 255.255.111.35 (合法)

核心思维:深度限制 + 严格剪枝

回溯的模板依然是 for 循环横向遍历(切多长),递归纵向深入(切下一段)。但这里有两个显著不同:

1. 树的深度固定为 4 不像 LC 131 那样可以切任意多段,IP 地址只有 4 段。 所以,我们的 Base Case 不再仅仅是"切到了字符串末尾",还要加上 "当前已经切了 4 段" 的判断。

  • 成功 :切了 4 段,并且 startIndex 正好走到了字符串末尾。

  • 失败:切了 4 段,但字符串还有剩余;或者字符串切完了,但段数不够 4。

2. 严格的剪枝 (Pruning)for 循环尝试切一个子串 sub 时,以下情况直接剪枝(停止当前循环或跳过):

  • 长度限制:子串长度大于 3(IP段最大255,只有3位)。

  • 前导零 :子串长度大于 1 且以 '0' 开头(如 "01")。

  • 数值限制:数值大于 255。

算法流程

  1. 状态变量

    • startIndex:当前从哪开始切。

    • path:存放已经切好的合法片段(例如 ["255", "255"])。

  2. Base Case

    • if (path.size() == 4)

      • 如果 startIndex == s.size():说明正好切完,拼装 path 为 IP 字符串,加入结果。

      • 否则:说明切了 4 段但还有剩余字符,非法,直接返回。

  3. 遍历尝试

    • for istartIndex 开始。

    • 长度剪枝 :如果 i - startIndex + 1 > 3,直接 break(后面更长,肯定不行)。

    • 截取子串sub = s.substr(startIndex, 长度)

    • 合法性校验

      • 检查是否有前导零。

      • 检查数值是否 > 255

    • 递归与回溯

      • 合法 -> path.push_back(sub) -> backtrack(i + 1) -> path.pop_back()

代码实现 (C++)

为了逻辑清晰,我使用 vector<string> path 来存储片段,最后拼接。虽然比起直接在原字符串插 . 稍微多一点点开销,但通用性强,完全沿用了上一题的模板,极易理解。

C++

复制代码
#include <vector>
#include <string>

using namespace std;

class Solution {
private:
    vector<string> res;
    vector<string> path; // 存储切好的每一段,如 ["255", "255", "11", "135"]

    // 判断子串是否合法
    bool isValid(const string& sub) {
        // 1. 长度校验
        if (sub.length() > 3) return false;
        
        // 2. 前导零校验 (长度大于1时,第一位不能是0)
        if (sub.length() > 1 && sub[0] == '0') return false;
        
        // 3. 数值校验 (不能大于 255)
        // 注意:stoll 可能溢出,但因为限制了长度<=3,所以 int 足够
        int num = stoi(sub);
        if (num > 255) return false;
        
        return true;
    }

    void backtrack(const string& s, int startIndex) {
        // 1. Base Case: 已经切了4段
        if (path.size() == 4) {
            // 必须正好切完整个字符串才算成功
            if (startIndex == s.size()) {
                string ip = path[0] + "." + path[1] + "." + path[2] + "." + path[3];
                res.push_back(ip);
            }
            return;
        }

        // 2. 遍历切割点
        // i 是当前切割子串的结束位置
        for (int i = startIndex; i < s.size(); ++i) {
            // 剪枝:如果当前切的长度超过3,后面肯定也不行,直接 break
            if (i - startIndex + 1 > 3) {
                break;
            }

            string sub = s.substr(startIndex, i - startIndex + 1);

            // 合法性校验
            if (isValid(sub)) {
                // 做选择
                path.push_back(sub);
                
                // 递归
                backtrack(s, i + 1);
                
                // 撤销选择
                path.pop_back();
            } else {
                // 如果当前子串不合法(比如是 "01" 或 "256")
                // 那么继续往后切只会更不合法(变成 "01..." 或 "256..."),
                // 但如果是前导0错误,其实可以直接 break。
                // 简单起见,这里 continue 或者 break 依据具体逻辑。
                // 对于前导0和数值过大,其实后面肯定都不行了,break 效率更高。
                // 为了通用性,用 continue 也没大错,但 break 更优。
            }
        }
    }

public:
    vector<string> restoreIpAddresses(string s) {
        res.clear();
        path.clear();
        
        // 简单剪枝:IP地址最多12位,最少4位
        if (s.size() > 12 || s.size() < 4) {
            return {};
        }
        
        backtrack(s, 0);
        return res;
    }
};

深度复杂度分析

  • 时间复杂度:常数级。

    • IP 地址固定分为 4 段,每段长度最多为 3。

    • 整棵决策树的高度是固定的 4。每层的分支数最多是 3(切1位、切2位、切3位)。

    • 总的状态数是极少的(34=81 种可能的切割方式,再乘上校验时间)。

    • 虽然看起来是 O(3N),但因为深度固定,其实可以看作 O(1)。

  • 空间复杂度:O(1)。

    • 递归栈深度固定为 4。

总结:精准的"外科手术"

今天这道题,让我们体会到了回溯算法中的**"精细化操作"**。

  • LC 131 分割回文串:只管切,只要是回文就行。

  • LC 93 复原 IP :不仅要切,还要管切了几刀 (深度控制),还要管切下来的肉好不好吃(数值校验)。

这种在递归过程中,通过path.size()来严格控制递归深度的技巧,在后续的棋盘问题(如 N 皇后)中也会用到。

下一篇,我们将离开"切割"问题,去挑战回溯算法中最经典、最能体现"二维矩阵搜索"魅力的棋盘游戏 ------N 皇后问题。准备好在棋盘上排兵布阵了吗?

下期见!

相关推荐
Hcoco_me8 小时前
大模型面试题17:PCA算法详解及入门实操
算法
跨境卫士苏苏8 小时前
亚马逊AI广告革命:告别“猜心”,迎接“共创”时代
大数据·人工智能·算法·亚马逊·防关联
程序员小白条8 小时前
0经验如何找实习?
java·开发语言·数据结构·数据库·链表
云雾J视界9 小时前
当算法试图解决一切:技术解决方案主义的诱惑与陷阱
算法·google·bert·transformer·attention·算法治理
Xの哲學9 小时前
Linux Miscdevice深度剖析:从原理到实战的完整指南
linux·服务器·算法·架构·边缘计算
夏乌_Wx9 小时前
练题100天——DAY23:存在重复元素Ⅰ Ⅱ+两数之和
数据结构·算法·leetcode
立志成为大牛的小牛9 小时前
数据结构——五十六、排序的基本概念(王道408)
开发语言·数据结构·程序人生·算法
a努力。10 小时前
Redis Java 开发系列#2 数据结构
java·数据结构·redis
沿着路走到底10 小时前
将数组倒序,不能采用reverse,算法复杂度最低
算法