#93 回溯 中等

复原 IP 地址

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

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

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

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

示例 1

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2

输入:s = "0000"
输出:["0.0.0.0"]

示例 3

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

💭 模拟答题者思考

1. 我先想暴力:在 s 的相邻字符间插入 3 个点,共有 C(n-1,3) 种切法,每种检查四段是否都在 0~255 且无前导零——思路对,但要把「切法枚举」写清楚。

2. 重复在哪里?很多切法在第一段就非法(如 "01"、"256"),却仍会继续枚举后面段的组合,浪费分支。

3. 关键转化:按从左到右顺序切四段,每段只可能是 1~3 个字符;用 segments 记录已切段,start 表示下一段起点——子问题变成「在 s[start:] 里再切 4 - len(segments) 段」。

4. 合法性一眼判断:len(seg)>1 and seg[0]=='0' 非法;int(seg)>255 非法;还剩 k 段时,剩余字符数必须在 [k, 3k] 之间,否则剪枝。

5. 例 s="25525511135":第一段只能试 "2"/"25"/"255",选 "255" 后递归;最终得到 255.255.11.135 与 255.255.111.35 两条合法路径。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
segmentslist<str>定义:当前已切出的 IP 段(尚未加点拼接)
维护:DFS 每确定一段合法子串就 append,回溯时 pop
更新:长度达到 4 且 start == len(s) 时,用 '.'.join(segments) 写入答案
startint定义:下一段在 s 中的起始下标(含)
维护:每选定长度为 len 的一段后,子问题从 start + len 继续
更新:初始为 0;每成功切一段则前进;与 len(s) 对齐时才能收集四段解
segstr定义:本轮尝试从 s[start:] 截取的长度为 1~3 的候选段
维护:仅当通过合法性校验才进入递归
更新:每轮 for length in 1..3 生成;非法(前导零、>255、越界)直接 continue
anslist<str>定义:所有合法的四段 IP 字符串
维护:只在「恰好 4 段且用完整个 s」时追加
更新:中途三段、或四段但还剩字符,均不收集

⌨️ 落码步骤

1. 初始化 ans = [],定义辅助函数 valid(seg):长度 1~3、无前导零、数值 ≤ 255

2. 定义 DFS backtrack(start, segments):若 len(segments) == 4,当且仅当 start == len(s) 时把 '.'.join(segments) 加入 ans,然后返回

3. 设还剩 remain = 4 - len(segments) 段;若剩余字符 len(s)-start 不在 [remain, 3*remain] 内,剪枝返回

4. 对 length 从 1 到 3:取 seg = s[start:start+length],越界或 not valid(seg) 则跳过

5. segments.append(seg),递归 backtrack(start + length, segments),回溯 segments.pop()

6. 从 backtrack(0, []) 启动,返回 ans

💻 代码实现

class Solution:
    def restoreIpAddresses(self, s: str) -> list[str]:
        ans: list[str] = []
        n = len(s)

        def valid(seg: str) -> bool:
            if len(seg) > 1 and seg[0] == "0":
                return False
            return 0 <= int(seg) <= 255

        def backtrack(start: int, segments: list[str]) -> None:
            if len(segments) == 4:
                if start == n:
                    ans.append(".".join(segments))
                return

            remain = 4 - len(segments)
            rest = n - start
            if rest < remain or rest > 3 * remain:
                return

            for length in range(1, 4):
                if start + length > n:
                    break
                seg = s[start : start + length]
                if not valid(seg):
                    continue
                segments.append(seg)
                backtrack(start + length, segments)
                segments.pop()

        backtrack(0, [])
        return ans
class Solution {
public:
    vector<string> restoreIpAddresses(string s) {
        vector<string> ans;
        vector<string> segments;
        int n = s.size();

        auto valid = [](const string& seg) {
            if (seg.size() > 1 && seg[0] == '0') return false;
            int val = stoi(seg);
            return val >= 0 && val <= 255;
        };

        function<void(int)> dfs = [&](int start) {
            if ((int)segments.size() == 4) {
                if (start == n) {
                    ans.push_back(segments[0] + "." + segments[1] + "." +
                                  segments[2] + "." + segments[3]);
                }
                return;
            }
            int remain = 4 - (int)segments.size();
            int rest = n - start;
            if (rest < remain || rest > 3 * remain) return;

            for (int len = 1; len <= 3; ++len) {
                if (start + len > n) break;
                string seg = s.substr(start, len);
                if (!valid(seg)) continue;
                segments.push_back(seg);
                dfs(start + len);
                segments.pop_back();
            }
        };

        dfs(0);
        return ans;
    }
};
// 时间 O(1)(段数与每段长度有常数上界),空间 O(1) 递归栈

📈 复杂度分析

时间复杂度 O(1)(至多 4 段、每段至多 3 种切分长度,与 |s|≤20 有常数上界;不计输出规模)
空间复杂度 O(1)(递归深度 ≤ 4,不计输出)

⚠️ 常见坑

❗ 前导零:段 "01"、"00" 非法,但单字符 "0" 合法;必须写 len(seg)>1 and seg[0]=='0',不能只判断数值。

❗ 四段未用完字符串:例如 s="1111" 切成四段 1.1.1.1 时 start 必须等于 n;若 segments.size()==4 但还有剩余字符,不能收集。

❗ 段长超过 3:IP 每段最多三位;循环 length 只取 1~3,且 int(seg)>255 时即使三位也要丢弃(如 "256")。

🔍 必测边界 Case

Case 1:全零
s = "0000" → ["0.0.0.0"](每段只能是单个 0)
Case 2:无解
s = "111" → [](至少 4 段、每段至少 1 字符,长度不足)
Case 3:示例 1
s = "25525511135" → 两组解,注意第三段可以是 11 或 111
Case 4:多解混合段长
s = "101023" → 5 组解,段长 1~3 混用