139. 单词拆分
难度中等
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
DFS 思路
- "leetcode"能否break,可以拆分为:"l"是否是单词表的单词、剩余子串能否break,"le"是否是单词表的单词、剩余子串能否break……
- 用 DFS 回溯,考察所有的拆分可能,指针从左往右扫描 s 串:
- 如果指针的左侧部分,是单词表中的单词,则对以指针为开头的剩余子串,递归考察。
- 如果指针的左侧部分,不是单词表的单词,不用看了,回溯,考察别的分支。
DFS 代码 超时
通过23/36个用例,遇到这个测试用例超时了:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,[“a”,“aa”,“aaa”,“aaaa”,“aaaaa”,“aaaaaa”,“aaaaaaa”,“aaaaaaaa”,“aaaaaaaaa”,“aaaaaaaaaa”]
const wordBreak = (s, wordDict) => {
const wordSet = new Set(wordDict);
const check = (s, wordSet, start) => {
// 检查从start开始的子串能否break
// 指针越界,结束递归
if (start == s.length) return true;
for (let end = start + 1; end <= s.length; end++) {
// end指针划分两部分,word是前缀部分
const word = s.slice(start, end);
// 如果前缀部分是单词表的词,且剩余子串能break,则返回true
if (wordSet.has(word) && check(s, wordSet, end)) return true;
}
// end指针怎么划分都没返回true,则返回false
return false;
};
return check(s, wordSet, 0);
};
给递归增加记忆化
- 下面这个例子中,做了大量重复计算:
- 这个递归是前序遍历的,遍历到右侧子树时,前面其实都已经计算过了
- 用一个数组,去存之前计算的结果,数组索引对应指针位置,值对应子调用的结果。下次遇到相同的子问题,直接返回数组中的缓存值
DFS + 记忆化 代码
const wordBreak = (s, wordDict) => {
const wordSet = new Set(wordDict);
const memo = new Array(s.length);
const check = (s, wordSet, start, memo) => {
if (start == s.length) return true;
if (memo[start] !== undefined) return memo[start];
for (let end = start + 1; end <= s.length; end++) {
const word = s.slice(start, end);
if (wordSet.has(word) && check(s