题目描述
思路解析
一句话答案:LeetCode 140 单词拆分 II 返回把 s 拆成词典单词的所有句子:记忆化搜索,memo[i] 存从下标 i 起能拼出的全部句子,枚举前缀词在词典就递归后缀拼接,起点只算一次避免指数重算,O(n³+输出量)。
这道题到底要把 s 拆出什么
给字符串 s 和词典 wordDict,把 s 切成若干段、每段是词典单词、拼回 s,列出所有句子。s="catsanddog"、词典 [cat,cats,and,sand,dog] 拼成 "cats and dog" 和 "cat sand dog";拼不出(如 "catsandog")返回 []。不是判定题。
把所有切法枚举一遍为什么会炸
每个位置都能切一刀或往下走,二选一分叉,切法随串长指数膨胀,数不完。更亏的是同一截后缀被反复重算:不同前缀走到同一位置,剩下那段被从头重拆。把「某位置起能拼哪些句子」算一次存下复用,重复展开就压下去。
状态定成「从下标 i 起能拼出的所有句子」
递归函数 dfs(i)(递归即函数调用自己、拆成同类小问题):返回后缀 s[i..] 能拼出的所有句子,答案是 dfs(0)。
为什么按起点 i 记账?后缀能拼哪些句子只跟起点下标有关,跟前缀怎么走到无关:"cat" 或 "cats" 走到下标 7,剩下的 "dog" 拼法都一样。给每个起点配 memo[i](记忆化:算过的句子列表存下、下次直接取),起点只 n+1 个、各算一次。
枚举前缀词递归后缀,空串为什么是成功标记
dfs(i) 从 i 往右枚举右端 j 取前缀词 s[i..j);词典先塞进哈希集合(O(1) 判断词在不在),词在里面就递归 dfs(j) 求后缀、把每句拼到词后。后缀非空拼「词 + 空格 + 后缀」,空串就只留词。
递归出口在 i 等于串长 n:整串拼满,返回只含空串的列表 [''],这是「拼完了」的成功标记,让上层的词有东西可接。若某起点一个词都接不出返回 [],才是此路不通。[''] 有一个空句子、[] 一个都没有。
拿 catsanddog 亲手走一遍调用栈
顺着调用栈(函数一层层调自己形成的那摞调用)走:s="catsanddog"。dfs(0) 试 "cat" 递归 dfs(3),dfs(3) 试 "sand"(下标 3 到 7)递归 dfs(7),dfs(7) 试 "dog" 递归 dfs(10),dfs(10) 到串尾返回 ['']。
往回拼:dfs(7) 得 ["dog"] 存 memo[7],dfs(3) 拼 "sand",dfs(0) 收到 "cat sand dog"。dfs(0) 再试 "cats" 递归 dfs(4),dfs(4) 试 "and" 又要 dfs(7),这次 memo[7] 已缓存直接返回,拼成 "and dog",dfs(0) 收到 "cats and dog"。最终返回这两句。
复杂度为什么甩不掉「输出量」,['']和[]差一个空串却天差地别
每个起点算一次,但内层枚举右端 j、每次要构造并哈希子串 s[i..j),构造就耗 O(j−i),枚举加构造升到 O(n³);句子可能指数级多,整体 O(n³ + 输出量)。空间是递归深度 O(n) 加 memo。
整串本身是词时 dfs 返回它这一句;拼不通那层返回 [],dfs(0) 得空列表;单字符同理。最阴的是拿 [''] 当失败:它是拼满整串的成功信号,误当空列表,句子会在最后一层被丢光。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「枚举词典词切首段、递归拼后缀、memo 缓存起点结果」,下面看调用栈逐层展开。
调用 dfs(0):求从下标 0(灰色之后)开始能拼出的所有句子,压入调用栈。
在 dfs(0) 里试切词 "cat"(下标 0 到 2):它在词典里!接着递归 dfs(3) 求后缀 "sanddog" 的所有句子。
调用 dfs(3):求从下标 3(灰色之后)开始能拼出的所有句子,压入调用栈。
在 dfs(3) 里试切词 "sand"(下标 3 到 6):它在词典里!接着递归 dfs(7) 求后缀 "dog" 的所有句子。
调用 dfs(7):求从下标 7(灰色之后)开始能拼出的所有句子,压入调用栈。
在 dfs(7) 里试切词 "dog"(下标 7 到 9):它在词典里!接着递归 dfs(10) 求后缀 "" 的所有句子。
调用 dfs(10):求从下标 10(灰色之后)开始能拼出的所有句子,压入调用栈。
i=10 已到串尾,说明前面的词正好把整串拼完。直接返回 [""](一个空句子,成功标记),这是递归出口,不写入 memo。
dfs(10) 返回 1 个后缀句子,把 "dog" 拼到每个前面,dfs(7) 目前收集到 1 个句子。
dfs(7) 的所有切法试完,得到 1 个句子,写入 memo[7] 并返回,弹出调用栈。
dfs(7) 返回 1 个后缀句子,把 "sand" 拼到每个前面,dfs(3) 目前收集到 1 个句子。
dfs(3) 的所有切法试完,得到 1 个句子,写入 memo[3] 并返回,弹出调用栈。
dfs(3) 返回 1 个后缀句子,把 "cat" 拼到每个前面,dfs(0) 目前收集到 1 个句子。
在 dfs(0) 里试切词 "cats"(下标 0 到 3):它在词典里!接着递归 dfs(4) 求后缀 "anddog" 的所有句子。
调用 dfs(4):求从下标 4(灰色之后)开始能拼出的所有句子,压入调用栈。
在 dfs(4) 里试切词 "and"(下标 4 到 6):它在词典里!接着递归 dfs(7) 求后缀 "dog" 的所有句子。
到达 dfs(7),memo[7] 已有缓存,直接返回,不再展开(这正是记忆化省时的关键)。
dfs(7) 返回 1 个后缀句子,把 "and" 拼到每个前面,dfs(4) 目前收集到 1 个句子。
dfs(4) 的所有切法试完,得到 1 个句子,写入 memo[4] 并返回,弹出调用栈。
dfs(4) 返回 1 个后缀句子,把 "cats" 拼到每个前面,dfs(0) 目前收集到 2 个句子。
dfs(0) 的所有切法试完,得到 2 个句子,写入 memo[0] 并返回,弹出调用栈。
调用栈全部弹空,dfs(0) 返回最终答案:共 2 个句子:"cat sand dog"、"cats and dog"。记忆化让每个起点只算一次,把可能指数级的重复展开压了下来。
边界:整串是词返回它;拼不通返回空;单字符同理。
两个延伸:先 I 判可行性剪枝;II 求方案故需记忆化返回列表。
参考代码
from typing import Listfrom functools import lru_cacheclass Solution: def wordBreak(self, s: str, wordDict: List[str]) -> List[str]: words = set(wordDict) @lru_cache(None) def dfs(i: int): if i == len(s): return [''] ans = [] for j in range(i + 1, len(s) + 1): word = s[i:j] if word in words: for tail in dfs(j): ans.append(word if not tail else word + ' ' + tail) return ans return dfs(0)复杂度
- 时间:O(n³ + 输出量),n 是串长。每个起点 i 只算一次(记忆化),但内层枚举右端 j、且每次都要构造并哈希子串 s[i..j)(本身就要 O(j−i) 的时间),所以光「枚举 + 查词」就是 O(n³)(也可记最大词长 L、写成 O(n²·L));若不计子串构造、只看集合查询才是 O(1)。再加上合法句子可能指数级,拼接与收集的代价取决于输出量,故整体 O(n³ + 输出量)
- 空间:O(n + 输出量),递归深度 O(n),memo 与最终句子列表占「输出量」级空间
易错点
面试追问把动画讲成自己的话
追问能不能先用「单词拆分 I」判断有没有解,再决定要不要枚举句子?
追问为什么这道题不能像普通 DP 那样只用一维布尔数组解决?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
地下城游戏
LeetCode 174 · 困难 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题