昨天晚上十一点多,我在公司楼下抽烟,我们组那个老李突然拦住我:哥,这道Word Break咋写啊,我脑壳疼。
我扫了一眼代码,心想这题挺经典的,来,我给你捋一捋。
##算法题:Word Break
题目说给你一个字符串 s 和一个单词列表 wordDict。
你要判断 s 能不能被拆分成若干个 wordDict 里的单词。
拆分时可以重复使用字典中的单词。
举例说明:
说白了就是:把字符串切成几段,每段都要在字典里能找到。
暴力解法:枚举所有可能的拆分方式,检查每种方式是否都在字典里。2^n种拆分方式,每段检查O(1)(用哈希表),时间复杂度 O(2^n)。n=100时直接爆。
优化思路 - 动态规划:用 dp[i] 表示 s[0:i] 能否被拆分。dp[0]=true(空字符串可以拆分)。对于每个位置i,遍历所有可能的分割点j,如果 dp[j]==true 且 s[j:i] 在字典里,则 dp[i]=true。时间复杂度 O(n²),空间复杂度 O(n)。n=10000时就是一亿次操作,可以接受。
核心思想是:如果前半部分能拆分,而且后半部分是字典里的词,那整个就能拆分。
package main
import (
"fmt"
)
// wordBreak 判断字符串能否被拆分成字典中的单词
func wordBreak(s string, wordDict []string) bool {
// 将字典转为哈希集合,方便查找
wordSet := make(map[string]bool)
for _, word := range wordDict {
wordSet[word] = true
}
n := len(s)
dp := make([]bool, n+1)
dp[0] = true// 空字符串可以拆分
for i := 1; i <= n; i++ {
// 遍历所有可能的分割点j
for j := 0; j < i; j++ {
// 如果前半部分能拆分,且后半部分在字典里
if dp[j] && wordSet[s[j:i]] {
dp[i] = true
break// 找到一种拆分方式就行
}
}
}
return dp[n]
}
func main() {
// 测试用例
fmt.Println(wordBreak("leetcode", []string{"leet", "code"})) // true
fmt.Println(wordBreak("applepenapple", []string{"apple", "pen"})) // true
fmt.Println(wordBreak("catsandog", []string{"cats", "dog", "sand", "and", "cat"})) // false
fmt.Println(wordBreak("aaaaaaa", []string{"aaaa", "aa"})) // true
fmt.Println(wordBreak("", []string{"a"})) // true
}
这题的关键点在于理解动态规划的状态转移。dp[i] 表示前 i 个字符能否拆分,如果某个位置 j 之前能拆分,而且 s[j:i] 是字典里的词,那 dp[i] 就能拆分。这和爬楼梯问题有点像,都是从子问题推导到当前问题。