【算法】回溯算法(一):从一道 IP 题到万能模板
摘要
回溯系列第一篇,入门。从「复原 IP 地址」这道经典题出发,完整走一遍回溯的标准建模流程:把约束拆干净 → 画决策树 → 套三要素 → 三层剪枝 → 写代码 → 踩题内小坑。最后沉淀出一份可复用的回溯模板------八字口诀「判终、砍支、循环三连」,加上决策三形态和三验证。本篇是纯入门,一次车都不翻;翻车实录在第二篇,五个坑一个比一个深。
说明,以下环境基于 go1.21+,题目均来自 LeetCode。配套代码仓库(按题号分目录):https://github.com/a18792721831/studyleetCode
系列篇目:
- 一、从一道 IP 题到万能模板(本篇)
- 二、五个坑与一次验收------决策、切片与去重
- 三、三记重锤与 N 皇后------记忆化、状态设计与三层漏斗
【算法】回溯算法(一):从一道 IP 题到万能模板
- [【算法】回溯算法(一):从一道 IP 题到万能模板](#【算法】回溯算法(一):从一道 IP 题到万能模板)
-
- 摘要
- [1. 起点:复原 IP 地址](#1. 起点:复原 IP 地址)
-
- [1.1 问题描述](#1.1 问题描述)
- [1.2 问题分析:先把约束拆干净](#1.2 问题分析:先把约束拆干净)
- [1.3 回溯思路:把问题画成一棵树](#1.3 回溯思路:把问题画成一棵树)
- [1.4 回溯三要素](#1.4 回溯三要素)
- [1.5 三层剪枝](#1.5 三层剪枝)
- [2. Go 实现](#2. Go 实现)
- [3. 复杂度分析](#3. 复杂度分析)
- [4. 沉淀:回溯模板](#4. 沉淀:回溯模板)
-
- [4.1 决策三形态](#4.1 决策三形态)
- [4.2 三验证](#4.2 三验证)
- [4.3 同一个骨架的七道题](#4.3 同一个骨架的七道题)
- 总结
- 参考资料
1. 起点:复原 IP 地址
1.1 问题描述
题目很简单,一句话:
给定一个只包含数字的字符串
s,返回所有可能的有效 IP 地址,这些地址可以通过在s中插入.来形成。不能重新排序或删除s中的任何数字。
几个示例:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
输入:s = "0000"
输出:["0.0.0.0"]
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
说白了,就是在字符串里找 3 个位置插点,把原串切成 4 段,要求每段都是一个合法的 IPv4 地址段。
1.2 问题分析:先把约束拆干净
拿到这道题,先别急着写代码,把「合法 IP 段」的定义拆成可执行的条件。一段数字合法,必须同时满足下面 4 条:
| 约束 | 说明 | 反例 |
|---|---|---|
| 长度 1 ∼ 3 1 \sim 3 1∼3 位 | 每段最多 3 个数字 | 4 位数字必然超过 255 |
| 数值 0 ∼ 255 0 \sim 255 0∼255 | 越界非法 | 312 非法 |
| 无前导零 | 长度大于 1 时首字符不能是 0 |
01、00 非法,但 0 合法 |
| 字符恰好用完 | 4 段拼起来必须等于原串 | 不能有剩余字符,也不能不够分 |
由最后一条还能推出一个全局剪枝 :合法输入的长度必须在 4 , 12 4, 12 4,12 之间。最短是 0.0.0.0(4 个字符),最长是 255.255.255.255(12 个字符)。长度不在这个区间的输入,连回溯都不用进,直接返回空。
打个比方,这个约束表就是「裁判规则」:回溯过程中每切出一段,裁判就照着表过一遍,任何一条不满足,这条分支立刻毙掉。
1.3 回溯思路:把问题画成一棵树
回溯的本质就是决策树的深度优先遍历(DFS)。这道题的决策过程是:每一层决定一段 IP,每层最多 3 个分支(取 1 位、2 位、3 位),树的最大深度固定是 4。
以 s = "25525511135" 为例,决策树长这样(? 表示还没填的段,虚线是走不通被剪掉的分支):
#mermaid-svg-rEBu5TKTEJB0HXVI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rEBu5TKTEJB0HXVI .error-icon{fill:#552222;}#mermaid-svg-rEBu5TKTEJB0HXVI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rEBu5TKTEJB0HXVI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rEBu5TKTEJB0HXVI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rEBu5TKTEJB0HXVI .marker.cross{stroke:#333333;}#mermaid-svg-rEBu5TKTEJB0HXVI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rEBu5TKTEJB0HXVI p{margin:0;}#mermaid-svg-rEBu5TKTEJB0HXVI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster-label text{fill:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster-label span{color:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster-label span p{background-color:transparent;}#mermaid-svg-rEBu5TKTEJB0HXVI .label text,#mermaid-svg-rEBu5TKTEJB0HXVI span{fill:#333;color:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI .node rect,#mermaid-svg-rEBu5TKTEJB0HXVI .node circle,#mermaid-svg-rEBu5TKTEJB0HXVI .node ellipse,#mermaid-svg-rEBu5TKTEJB0HXVI .node polygon,#mermaid-svg-rEBu5TKTEJB0HXVI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rEBu5TKTEJB0HXVI .rough-node .label text,#mermaid-svg-rEBu5TKTEJB0HXVI .node .label text,#mermaid-svg-rEBu5TKTEJB0HXVI .image-shape .label,#mermaid-svg-rEBu5TKTEJB0HXVI .icon-shape .label{text-anchor:middle;}#mermaid-svg-rEBu5TKTEJB0HXVI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rEBu5TKTEJB0HXVI .rough-node .label,#mermaid-svg-rEBu5TKTEJB0HXVI .node .label,#mermaid-svg-rEBu5TKTEJB0HXVI .image-shape .label,#mermaid-svg-rEBu5TKTEJB0HXVI .icon-shape .label{text-align:center;}#mermaid-svg-rEBu5TKTEJB0HXVI .node.clickable{cursor:pointer;}#mermaid-svg-rEBu5TKTEJB0HXVI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rEBu5TKTEJB0HXVI .arrowheadPath{fill:#333333;}#mermaid-svg-rEBu5TKTEJB0HXVI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rEBu5TKTEJB0HXVI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rEBu5TKTEJB0HXVI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rEBu5TKTEJB0HXVI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rEBu5TKTEJB0HXVI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rEBu5TKTEJB0HXVI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster text{fill:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI .cluster span{color:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rEBu5TKTEJB0HXVI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rEBu5TKTEJB0HXVI rect.text{fill:none;stroke-width:0;}#mermaid-svg-rEBu5TKTEJB0HXVI .icon-shape,#mermaid-svg-rEBu5TKTEJB0HXVI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rEBu5TKTEJB0HXVI .icon-shape p,#mermaid-svg-rEBu5TKTEJB0HXVI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rEBu5TKTEJB0HXVI .icon-shape .label rect,#mermaid-svg-rEBu5TKTEJB0HXVI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rEBu5TKTEJB0HXVI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rEBu5TKTEJB0HXVI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rEBu5TKTEJB0HXVI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2
25
255
255
11
111
135
35
剩余10字符 > 3×3上限
子分支全部越界或超长
25525511135
2.?.?.?
25.?.?.?
255.?.?.?
255.255.?.?
255.255.11.?
255.255.111.?
255.255.11.135 ✓
255.255.111.35 ✓
剪枝 ✗
剪枝 ✗
三个根分支的命运各不相同:
2开头 :只用了 1 个字符,剩余 10 个字符要塞进 3 段,但 3 段最多容纳 3 × 3 = 9 3 \times 3 = 9 3×3=9 个字符,可行性剪枝直接整支剪掉25开头 :第 2 段取525数值越界,取5/52后续也分配不下去,所有子分支都被剪光255开头 :一路合法往下走,第 3 段取11配第 4 段135、取111配第 4 段35,收获两个合法解
1.4 回溯三要素
套回溯的标准框架,三要素在这道题里的对应关系:
| 要素 | 本题对应 |
|---|---|
| 路径 | 已经确定的段列表 path,比如 ["255", "255"] |
| 选择列表 | 从当前位置 start 开始,取 1 位、2 位或 3 位子串 |
| 结束条件 | len(path) == 4 && start == len(s),凑够 4 段且字符恰好用完 |
这里有个最容易写错的点 :结束条件是「与」不是「或」。len(path) == 4 只说明段数够了,还必须检查 start == len(s) 确认字符刚好用完。比如 "25525511",切出 2.5.5.2 后还剩 5511 没用,这种组合必须丢弃。反过来,字符用完了但段数不够 4,同样作废。
1.5 三层剪枝
剪枝是回溯的精髓,这道题可以做三层:
- 全局剪枝 :
len(s) < 4 || len(s) > 12,直接返回空,搜索都不用开始。 - 可行性剪枝 :设剩余字符数为 r e m a i n remain remain,还需填的段数为 n e e d need need,则必须满足 n e e d ≤ r e m a i n ≤ 3 × n e e d need \le remain \le 3 \times need need≤remain≤3×need。剩余字符比段数还少,肯定不够分;剩余字符超过 3 × n e e d 3 \times need 3×need,肯定装不下。这个剪枝能砍掉大量注定失败的分支。
- 合法性剪枝 :切出的段做两个检查------前导零(
len(seg) > 1 && seg[0] == '0')和数值上限(> 255)。
#mermaid-svg-dJjuH71qL8VDb1Hr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dJjuH71qL8VDb1Hr .error-icon{fill:#552222;}#mermaid-svg-dJjuH71qL8VDb1Hr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dJjuH71qL8VDb1Hr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dJjuH71qL8VDb1Hr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dJjuH71qL8VDb1Hr .marker.cross{stroke:#333333;}#mermaid-svg-dJjuH71qL8VDb1Hr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dJjuH71qL8VDb1Hr p{margin:0;}#mermaid-svg-dJjuH71qL8VDb1Hr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster-label text{fill:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster-label span{color:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster-label span p{background-color:transparent;}#mermaid-svg-dJjuH71qL8VDb1Hr .label text,#mermaid-svg-dJjuH71qL8VDb1Hr span{fill:#333;color:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr .node rect,#mermaid-svg-dJjuH71qL8VDb1Hr .node circle,#mermaid-svg-dJjuH71qL8VDb1Hr .node ellipse,#mermaid-svg-dJjuH71qL8VDb1Hr .node polygon,#mermaid-svg-dJjuH71qL8VDb1Hr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dJjuH71qL8VDb1Hr .rough-node .label text,#mermaid-svg-dJjuH71qL8VDb1Hr .node .label text,#mermaid-svg-dJjuH71qL8VDb1Hr .image-shape .label,#mermaid-svg-dJjuH71qL8VDb1Hr .icon-shape .label{text-anchor:middle;}#mermaid-svg-dJjuH71qL8VDb1Hr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dJjuH71qL8VDb1Hr .rough-node .label,#mermaid-svg-dJjuH71qL8VDb1Hr .node .label,#mermaid-svg-dJjuH71qL8VDb1Hr .image-shape .label,#mermaid-svg-dJjuH71qL8VDb1Hr .icon-shape .label{text-align:center;}#mermaid-svg-dJjuH71qL8VDb1Hr .node.clickable{cursor:pointer;}#mermaid-svg-dJjuH71qL8VDb1Hr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dJjuH71qL8VDb1Hr .arrowheadPath{fill:#333333;}#mermaid-svg-dJjuH71qL8VDb1Hr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dJjuH71qL8VDb1Hr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dJjuH71qL8VDb1Hr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJjuH71qL8VDb1Hr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dJjuH71qL8VDb1Hr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJjuH71qL8VDb1Hr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster text{fill:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr .cluster span{color:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dJjuH71qL8VDb1Hr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dJjuH71qL8VDb1Hr rect.text{fill:none;stroke-width:0;}#mermaid-svg-dJjuH71qL8VDb1Hr .icon-shape,#mermaid-svg-dJjuH71qL8VDb1Hr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJjuH71qL8VDb1Hr .icon-shape p,#mermaid-svg-dJjuH71qL8VDb1Hr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dJjuH71qL8VDb1Hr .icon-shape .label rect,#mermaid-svg-dJjuH71qL8VDb1Hr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJjuH71qL8VDb1Hr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dJjuH71qL8VDb1Hr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dJjuH71qL8VDb1Hr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否,继续搜索
是
否
是
试下一个 length
否,试下一个
是,break
是
是
否
否,剪枝
进入 backtrack(start, path)
path 凑够 4 段?
剩余字符够分
且装得下?
取段长度 length
从 1 到 3 逐个试
start + length
超出串长?
段合法?
无前导零且 ≤ 255
做选择
path 追加 seg
递归下一层
backtrack(start + length, path)
撤销选择
path 弹出 seg
本次调用结束
返回上一层
字符刚好用完?
✓ 收集结果
✗ 丢弃
顺着主干从上往下读,就是一次完整的搜索过程:没凑够 4 段就先过剪枝检查,剪不掉就逐个长度试探,段合法就执行「做选择 → 递归 → 撤销」三连,再回头试下一个长度;凑够 4 段则走右侧出口,字符刚好用完才收集结果。
2. Go 实现
完整代码,可直接复制运行:
go
package main
import (
"fmt"
"strconv"
"strings"
)
// restoreIpAddresses 复原 IP 地址:回溯法
func restoreIpAddresses(s string) []string {
var result []string
// 全局剪枝:合法 IP 长度范围 [4, 12]
if len(s) < 4 || len(s) > 12 {
return result
}
// backtrack: start 是当前处理到的下标,path 是已确定的段
var backtrack func(start int, path []string)
backtrack = func(start int, path []string) {
// 终止条件:凑够 4 段,且字符恰好用完(两个条件缺一不可)
if len(path) == 4 {
if start == len(s) {
result = append(result, strings.Join(path, "."))
}
return
}
// 可行性剪枝:剩余字符必须能填满剩余段,且不超上限
remain := len(s) - start
need := 4 - len(path)
if remain < need || remain > 3*need {
return
}
// 每段尝试取 1~3 位
for length := 1; length <= 3; length++ {
if start+length > len(s) {
break
}
seg := s[start : start+length]
// 合法性剪枝:前导零 / 超过 255 直接跳过
if !isValidSegment(seg) {
continue
}
// 做选择
path = append(path, seg)
// 进入下一层
backtrack(start+length, path)
// 撤销选择
path = path[:len(path)-1]
}
}
backtrack(0, []string{})
return result
}
// isValidSegment 判断一段数字是否合法
func isValidSegment(seg string) bool {
// 前导零检查:"0" 合法,"01"/"00" 非法
if len(seg) > 1 && seg[0] == '0' {
return false
}
// 数值范围检查:0 ~ 255
val, _ := strconv.Atoi(seg)
return val <= 255
}
func main() {
testCases := []string{
"25525511135", // 常规用例
"0000", // 全零
"101023", // 多解
"1111", // 唯一解
"010010", // 前导零陷阱
"12345678901234", // 超长剪枝
}
for _, s := range testCases {
fmt.Printf("输入: %-16s 输出: %v\n", s, restoreIpAddresses(s))
}
}
运行结果:
输入: 25525511135 输出: [255.255.11.135 255.255.111.35]
输入: 0000 输出: [0.0.0.0]
输入: 101023 输出: [1.0.10.23 1.0.102.3 10.1.0.23 10.10.2.3 101.0.2.3]
输入: 1111 输出: [1.1.1.1]
输入: 010010 输出: [0.10.0.10 0.100.1.0]
输入: 12345678901234 输出: []
这道题内的四个小坑
- 前导零漏判 。
"0"合法、"01"非法,判断条件是len(seg) > 1 && seg[0] == '0'。如果写成seg[0] == '0'就非法,直接把0.0.0.0这个合法解干掉了。重点看010010这个用例:0.1.0.010、0.10.01.0这些组合全部被前导零检查拦下,只输出两个合法结果。 - 终止条件写成「或」 。只判断
len(path) == 4不判断字符用完,"25525511"会误收2.5.5.2;只判断字符用完不判断段数,凑不够 4 段的也会混进去。 - 忘了剪枝也能过,但不好。不加可行性剪枝,代码照样正确,就是会多跑很多无效分支。
- 用暴力三重循环切。枚举三个切割点也能做,但回溯写法更通用------把「4 段」换成「k 段」,回溯改一个参数就行,暴力要重构。
3. 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O ( 3 4 × ∣ s ∣ ) O(3^4 \times \lvert s \rvert) O(34×∣s∣) | 树最多 4 层,每层 3 个分支,叶子节点最多 3 4 = 81 3^4 = 81 34=81 个;每片叶子要 O ( ∣ s ∣ ) O(\lvert s \rvert) O(∣s∣) 拼字符串。段数固定,搜索空间是常数级 |
| 空间 | O ( ∣ s ∣ ) O(\lvert s \rvert) O(∣s∣) | 递归深度不超过 4,路径存储 O ( ∣ s ∣ ) O(\lvert s \rvert) O(∣s∣),不计结果集 |
这道题的搜索空间被「恰好 4 段」锁死了,所以时间复杂度其实非常低。但如果把问题泛化成「切成 k 段」,时间复杂度就变成:
T ( k ) = O ( 3 k × ∣ s ∣ ) T(k) = O(3^k \times \lvert s \rvert) T(k)=O(3k×∣s∣)
这时候剪枝的价值就体现出来了------可行性剪枝能在中途就毙掉大量注定无解的分支。
4. 沉淀:回溯模板
IP 题做完,可以把回溯的通用模板提炼出来了。八个字口诀:判终、砍支、循环三连。
go
func backtrack(状态) {
// 1. 判终:到达叶子 → 满足要求则【拷贝快照】收集 → return
if 到达叶子 {
if 满足要求 {
res = append(res, 拷贝(path))
}
return
}
// 2. 砍支:可行性剪枝,子树必死直接 return
if 子树必死 {
return
}
// 3. 循环三连:做选择 → 递归 → 还原
for 选择 := range 选择列表 {
if 选择非法 { // 合法性剪枝
continue
}
path = append(path, 选择) // 做选择
backtrack(下一层状态) // 递归
path = path[:len(path)-1] // 还原
}
}
这份模板的每一处细节------「拷贝快照」四个字、「且」不是「或」------都对应后文的一次翻车。模板本身十分钟就能背下来,但背下来和会用是两回事。接下来用它在更多题上实战,然后你就看着我怎么翻车的。
4.1 决策三形态
往模板的「选择列表」格子里填东西,只有三种形态:
| 形态 | 决策问句 | 适用条件 | 代表题 |
|---|---|---|---|
| 选一个 | "从候选里挑哪个?" | 这一步必须恰好做一个选择 | 排列、组合、映射 |
| 要/不要 | "这个元素进不进?" | 每个元素独立地可选可不选 | 子集、01背包 |
| 切多长 | "下一段切几位?" | 序列切割 | IP、分割回文串 |
判断口诀一句话:"这个位置能空着吗?" 能空 → 要/不要;不能空 → 选一个/切一段。
4.2 三验证
决策设计完后,过三个验证才能定稿:
- 叶子 = 答案? 每条根到叶路径恰好生成一个完整候选解
- 无遗漏? 任何合法答案都存在路径可达(我的错误版在这条上挂掉)
- 约束局部可判? 每个决策点上,选择可不可行能用当前状态 O(1) 判断
4.3 同一个骨架的七道题
| 题目 | 路径 | 选择列表 | 结束条件 |
|---|---|---|---|
| 复原IP(LC93) | 已确定的段 | 取 1 ∼ 3 1 \sim 3 1∼3 位 | 4 段且字符用完 |
| 全排列(LC46) | 已排列的数 | 未使用的数 | 排列长度等于 n |
| 括号生成(LC22) | 已拼接的串 | 放 ( 或 ) |
长度等于 2n |
| 分割回文串(LC131) | 已切出的子串 | 下一段切在哪 | 切完整个字符串 |
| 单词搜索(LC79) | 已走的格子 | 上下左右 | 匹配完单词 |
| 火柴拼正方形(LC473) | 各桶当前长度 | 这根火柴放哪个桶 | 火柴放完 |
| N 皇后(LC51) | 已放的皇后 | 当前行哪一列 | 放完 n 行 |
同一个模板,换汤不换药。理论上把这一道题吃透,等于顺手带走了一批题------但实际上,我马上就在下一道题上翻车了。
总结
本篇从一道 IP 题走完了回溯的标准建模流程:约束拆解 → 决策树 → 三要素 → 三层剪枝 → 代码实现 → 模板沉淀。带走三样东西:
- 一份模板:判终、砍支、循环三连
- 一个决策分类法:选一个 / 要不要 / 切多长,口诀是"这个位置能空着吗"
- 一套验证:叶子=答案、无遗漏、约束局部可判
模板到手,看起来回溯也就这么回事。下一篇开始,用这个模板在五道题上实战,翻五个坑------决策粒度、切片引用、模型滥用、去重、范式识别------其中一个坑让 24 个排列丢了一半,另一个坑同一考点伏击了我三次。
参考资料
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。