当前:LC139 · 单词拆分 · 首次出现于 Day 33 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC139 · Word Break · 动态规划

单词拆分:前缀能拆,才允许在这里接一个词

dp[i] 表示 s 的前 i 个字符能否拆完。枚举最后一词的起点 j:dp[j] 为真且 s[j:i] 在词典里,dp[i] 就成立。

词典放进集合。dp[0]=true。对每个结尾 i,枚举 j∈[0,i),若 dp[j] 且 s[j:i] 在集合中,则 dp[i]=true 并停止枚举。答案 dp[n]。时间 O(n²),空间 O(n)。

时间 O(n²)空间 O(n)结论先行 · 全文约 6 节
导读

这是 LeetCode 139. Word Break。字符串 s 和一个字符串词典 wordDict,判断 s 能不能拆成一个或多个词典里的单词。单词可以重复使用,必须恰好拼满 s,不能剩字符,不能重叠使用同一段。

主例 s = "leetcode",wordDict = ["leet","code"]。前 4 个字符 leet 在词典里,后 4 个 code 也在,整串可拆,答案 true。演示先点亮 dp[4],再点亮 dp[8]。

第一直觉是从左往右贪心吃最长词。"aaaaaaa" 配 ["aaaa","aaa"] 时,先吃 4 个 a 会剩 3 个,其实 3+4 也能拼。缺的是「每个前缀是否可拆」的备忘,好让后面的词接在任意一个成功前缀后面。下面从这块缺口推出前缀 DP。

固定结尾,枚举最后一词从哪切开

整串可拆,当且仅当存在一种切法让每一段都在词典里。切点可以有很多,暴力递归会在同一前缀上反复问「你能不能拆」。这个布尔答案只依赖前缀长度,应该记下来。

缺的信息是 dp[j]:s[0:j] 是否已经能拆。没有它,你在位置 i 配到一个词典词 s[j:i],也不知道左边那截是否合法。贪心只保留一种切法,主例够用,带歧义的重复字符就会假阴性。

从缺口定义状态。dp[0]=true,空前缀视为拆好,这样第一个词可以从 0 切开。对每个 i=1..n,枚举 j=0..i−1:若 dp[j] 为真,并且 s[j:i] 在集合里,则 dp[i]=true,后面的 j 不必再看。词典先放进哈希集合,否则每次线性扫列表。

用手走主例。s 长度 8,dp 长度 9,起先只有 dp[0]=true。i 走到 4 之前,任何 s[0:i] 都不是 leet 或 code,全是 false。i=4,j=0,dp[0] 为真且 s[0:4]="leet" 在词典,dp[4]=true。演示第一帧 hit=leet。i=5、6、7 接不上新词。i=8,j=4,dp[4] 为真且 s[4:8]="code" 在词典,dp[8]=true。演示第二帧。整串可拆。

s="applepenapple"、词典含 apple 与 pen 时,dp 会在 5、8、13 依次为真,同一个 apple 用了两次,这是允许的。某前缀永远点不亮,后面即使有词也接不上。每个 (i,j) 对切一段子串,时间按 O(n²) 计;子串哈希按实现另计。Trie 可以把「从 j 出发能匹配哪些词」收成沿字符走,最坏仍和 n、词长有关。

dp[0] 必须是 true第一个词没有「再前面的前缀」。空前缀算拆好,leet 才能在 j=0 处合法切开。写成 false,主例 dp[4] 永远点不亮。
s = "leetcode", dict = [leet, code]
leetcode词典
leetcode
ldp[1]=F
edp[2]=F
edp[3]=F
tdp[4]=T
cdp[5]=F
odp[6]=F
ddp[7]=F
edp[8]=F
dp[4]=true:s[0:4]=leet 在词典

Go:DP + 词典集合

solution.goGo
func wordBreak(s string, wordDict []string) bool {
set := map[string]bool{}
for _, w := range wordDict { set[w] = true }
dp := make([]bool, len(s)+1)
dp[0] = true
for i := 1; i <= len(s); i++ {
for j := 0; j < i; j++ {
if dp[j] && set[s[j:i]] {
dp[i] = true
break
}
}
}
return dp[len(s)]
}

1词典进集合,判断 s[j:i] 是否成词是平均 O(1)。

2dp[0]=true。主例 i=4、j=0 时靠它点亮 leet。

3必须先有 dp[j],再接 s[j:i]。只看词在不在词典、不问左边,会把前缀垃圾也算成功。

4命中就 break。主例 i=8 在 j=4 接上 code,不必再试别的切点。

总结

前缀可拆才能接词。主例 dp[4] 吃 leet,dp[8] 吃 code,整串 true。

  • 贪心吃最长词会在重复字符上假阴性。每个前缀都要记能否拆。
  • dp[0]=true 是第一个词的切口。dp[n] 才是答案。
  • 词可重复使用:状态里没有「这个词用过没有」,只有前缀切没切开。
同族题目
LC140单词拆分 IILC208实现 TrieLC300最长递增子序列