单词拆分 图解题解
这道题到底在问什么
- 输入
- s="leetcode", dict=["leet","code"]
- 输出
- true
最优解:为什么这么做
一句话答案: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 思路是它的地基。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这一句:每个位置看两件事——前面某处能拆 + 中间一段成词。下面每一格都在套它。
- 4上行是 leetcode 每个字符(位置 0 到 8),下行 dp 待填。dp[i] 看的是「前 i 个字符」,从最左边开始。
- 5空串什么都不用拆,算 0 个词拼成,所以 dp[0]=true(✓)。这是所有切分的合法起点。
- 6判定 dp[1]:切点 j=0 处左边可拆,但中间 "l" 不在词典里,这个切法不行,再看别的切点。
- 7前 1 个字符 "l" 试遍所有切点都不行,dp[1]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 8判定 dp[2]:切点 j=0 处左边可拆,但中间 "le" 不在词典里,这个切法不行,再看别的切点。
- 9前 2 个字符 "le" 试遍所有切点都不行,dp[2]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 10判定 dp[3]:切点 j=0 处左边可拆,但中间 "lee" 不在词典里,这个切法不行,再看别的切点。
- 11前 3 个字符 "lee" 试遍所有切点都不行,dp[3]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 12判定 dp[4]:切点 j=0 处左边可拆(dp[0]=✓),中间这段 "leet" 正好是词典里的词 → dp[4] 成立!
- 13前 4 个字符 "leet" 能拆:在 j=0 处断开,左边已可拆、右边 "leet" 成词。dp[4]=✓。
- 14判定 dp[5]:切点 j=0 处左边可拆,但中间 "leetc" 不在词典里,这个切法不行,再看别的切点。
- 15判定 dp[5]:切点 j=4 处左边可拆,但中间 "c" 不在词典里,这个切法不行,再看别的切点。
- 16前 5 个字符 "leetc" 试遍所有切点都不行,dp[5]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 17判定 dp[6]:切点 j=0 处左边可拆,但中间 "leetco" 不在词典里,这个切法不行,再看别的切点。
- 18判定 dp[6]:切点 j=4 处左边可拆,但中间 "co" 不在词典里,这个切法不行,再看别的切点。
- 19前 6 个字符 "leetco" 试遍所有切点都不行,dp[6]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 20判定 dp[7]:切点 j=0 处左边可拆,但中间 "leetcod" 不在词典里,这个切法不行,再看别的切点。
- 21判定 dp[7]:切点 j=4 处左边可拆,但中间 "cod" 不在词典里,这个切法不行,再看别的切点。
- 22前 7 个字符 "leetcod" 试遍所有切点都不行,dp[7]=✗。但别急着放弃——后面更长的位置可能借到别的成词段。
- 23判定 dp[8]:切点 j=0 处左边可拆,但中间 "leetcode" 不在词典里,这个切法不行,再看别的切点。
- 24判定 dp[8]:切点 j=4 处左边可拆(dp[4]=✓),中间这段 "code" 正好是词典里的词 → dp[8] 成立!
- 25前 8 个字符 "leetcode" 能拆:在 j=4 处断开,左边已可拆、右边 "code" 成词。dp[8]=✓。
- 26最右 dp[8]=✓,说明整个 "leetcode" 能被拆完(leet + code)。dp[n] 就是最终答案。
⚠️ 容易写错的地方
✗ 错:看到能匹配就贪心切最长
✓ 对:必须枚举所有切点 j
最长匹配可能让后面卡死(如 "aaaa" 配 ["aaa","a"])
✗ 错:忘了 dp[j] 这个前提
✓ 对:左边 s[0:j] 也得能拆
只判中间成词不够,整条链都要可达
✗ 错:dp[0] 没设 true
✓ 对:dp[0]=true 是起点
第一个词靠它转移,缺了全盘判否
完整代码(Python / C++ / Java)
Python
def wordBreak(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]C++
bool wordBreak(string s, vector<string>& wordDict){
unordered_set<string> words(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true;
for(int i = 1; i <= n; ++i)
for(int j = 0; j < i; ++j)
if(dp[j] && words.count(s.substr(j, i - j))){
dp[i] = true; break;
}
return dp[n];
}Java
public boolean wordBreak(String s, List<String> wordDict){
Set<String> words = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for(int i = 1; i <= n; i++)
for(int j = 0; j < i; j++)
if(dp[j] && words.contains(s.substring(j, i))){
dp[i] = true; break;
}
return dp[n];
}复杂度
时间
O(n²·L)
外层 i、内层 j 共 O(n²) 个切点,每次取子串并查词典约 O(L)
空间
O(n)
dp 数组长度 n+1;词典用哈希集合
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单词拆分 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果还要列出所有拆分方案呢?+
那是 LC140 单词拆分 II,需要 DFS + 记忆化,记录从每个位置出发能拼出的所有句子。
怎么给这题提速?+
用「最大词长」限制内层 j 的范围,或用 Trie 沿字符走匹配,避免反复取子串。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 单词拆分 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。