题目描述
思路解析
一句话答案:LeetCode 139 单词拆分的标准解是布尔动态规划:dp[i] 表示 s 的前 i 个字符能否被词典完整切分,枚举最后一个词的起点 j,只要存在 dp[j] 为真且 s[j:i] 在词典里,dp[i] 就为真。贪心切最长词会让后面卡死,必须枚举所有切点。时间约 O(n² × L)、空间 O(n)。
单词拆分在判定什么
给字符串 s 和词典 wordDict,问能否把 s 从头到尾切成若干段,使每一段都恰好是词典里的词,词可以重复使用,答案只要 true 或 false。比如 s = "leetcode"、词典含 "leet" 和 "code",切成 leet + code 即为 true。注意必须整串切干净,多一个字符盖不住都算失败,也不允许段与段重叠或留缝。
为什么不能贪心地每次匹配最长的词
直觉做法是从左往右扫,每次贪心咬下能匹配的最长词。反例很快出现:s = "aaaa",词典是 ["aaa", "a"],贪心先咬 "aaa",剩下一个 "a" 能配上纯属侥幸;换成 s 长度稍变或词典稍变,先咬最长就把后面卡死,而 a + a + a + a 这样的切法明明存在。前面的选择会影响后面能不能切,单向贪心看不见这种牵连。
改成回溯搜索所有切法,最坏是指数级分支。但仔细看会发现大量重复:不同的前段切法常常在同一个位置会师,之后「剩下这一截能不能切」是同一个子问题,被反复求解。子问题只由「切到了哪个位置」决定——这就是重叠子问题加无后效性,动态规划的标准入口。
dp[i] 的含义与 dp[0] 为什么是 true
定义 dp[i] 为「s 的前 i 个字符(即 s[0:i])能否被词典完整切分」,i 从 0 到 n,最终答案是 dp[n]。下标是「长度」而非「字符位置」,好处是空前缀有了名分:dp[0] = true,表示空串用零个词就拼成了。它不是凑数的技巧,而是第一个词的转移依据——切下开头的 "leet" 时,靠的正是「它左边的空串已经合法」。漏设 dp[0],整张表推不起来,全盘皆否。
枚举最后一个词的转移为什么完备
算 dp[i] 时,盯住一种切法里的最后一个词:它必然从某个位置 j 开始、到 i 结束。于是「前 i 个字符可切」等价于「存在切点 j,使前 j 个字符可切,且 s[j:i] 恰好是词典里的词」。j 从 0 到 i-1 全枚举,任何合法切法的最后一刀都会被扫到,不存在漏判;找到一个成立的 j 就可以 break,因为 dp[i] 只关心存在性。
两个条件缺一不可:只查 s[j:i] 成词而不查 dp[j],就可能接在一段根本切不开的前缀后面,链条中间是断的。把词典先存进哈希集合,s[j:i] in words 的判断均摊 O(1) 次哈希、代价主要在取子串本身。
复杂度多少,还能怎么提速
外层 i、内层 j 共约 n²/2 对切点,每对要取子串并做哈希,长度记 L,总时间约 O(n² × L);dp 数组长 n + 1 加一个词典集合,空间 O(n) 级别。
提速有两条常用路:一是先算出词典里的最大词长 maxLen,内层 j 只需回看 i - maxLen 以内的范围,比它更早的切点不可能成词;二是把词典建成 Trie 前缀树,沿字符逐个下走做匹配,省掉反复取子串。若题目升级为列出所有具体拆分方案,那是 LeetCode 140 单词拆分 II,要用记忆化搜索去枚举句子,判定版的 dp 思路是它的地基。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
状态表示一个前缀能否完整拆分。枚举最后一个词的起点,同时检查前缀是否可拆、最后一段是否成词。
列号表示前缀长度。零列对应空串,第一列对应末字符 l,第八列对应末字符 e。
空前缀可以用零个单词组成,初始状态为真。第一个完整单词会用到这个起点。
前 0 个字符可拆,检查最后一段 l。它不在词典中,这个切点不能使当前前缀成立。
前 1 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 le。它不在词典中,这个切点不能使当前前缀成立。
前 2 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 lee。它不在词典中,这个切点不能使当前前缀成立。
前 3 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 leet。它在词典中,当前前缀可拆。
前 4 个字符可以完整拆分,记录为真。
前 0 个字符可拆,检查最后一段 leetc。它不在词典中,这个切点不能使当前前缀成立。
前 4 个字符可拆,检查最后一段 c。它不在词典中,这个切点不能使当前前缀成立。
前 5 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 leetco。它不在词典中,这个切点不能使当前前缀成立。
前 4 个字符可拆,检查最后一段 co。它不在词典中,这个切点不能使当前前缀成立。
前 6 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 leetcod。它不在词典中,这个切点不能使当前前缀成立。
前 4 个字符可拆,检查最后一段 cod。它不在词典中,这个切点不能使当前前缀成立。
前 7 个字符没有可行切点,记录为假;更长前缀仍可能包含完整单词。
前 0 个字符可拆,检查最后一段 leetcode。它不在词典中,这个切点不能使当前前缀成立。
前 4 个字符可拆,检查最后一段 code。它在词典中,当前前缀可拆。
前 8 个字符可以完整拆分,记录为真。
最后一个状态为真,整个字符串可以拆成 leet 和 code;中间有些前缀不可拆不影响这个结果。
每次判断都要求整个字符串被覆盖,不能留下无法组成单词的后缀,也不要求存在多种拆法。
判断可行性、统计方案数与输出全部方案是不同任务。它们可以共用切点思路,但状态和合并方式不同。
参考代码
class Solution: def wordBreak(self, s, wordDict): words = set(wordDict) n = len(s) dp = [False] * (n + 1) dp[0] = True for i in range(1, n + 1): for j in range(i): if dp[j] and s[j:i] in words: dp[i] = True break return dp[n]复杂度
- 时间:最坏 O(n³ + W),原实现枚举 O(n²) 个区间;复制子串与计算哈希还需 O(区间长度)。W 为词典字符总数。
- 空间:O(n + W),状态数组、临时子串与词典集合。哈希查询采用通常的平均开销分析。
易错点
面试追问把动画讲成自己的话
追问如果要统计方案数呢?
追问如何限制枚举范围?
追问如果要输出全部拆分方案呢?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最长递增子序列
LeetCode 300 · 中等 · 沿着 一维动态规划 继续往下推进