一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情
题目(Word Break II)
链接:https://leetcode-cn.com/problems/word-break-ii
解决数:719
通过率:52.4%
标签:字典树 记忆化搜索 哈希表 字符串 动态规划 回溯
相关公司:amazon facebook google
给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。
注意: 词典中的同一个单词可能在分段中被重复使用多次。
示例 1:
输入: s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
输出: ["cats and dog","cat sand dog"]
示例 2:
输入: s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"]
输出: ["pine apple pen apple","pineapple pen apple","pine applepen apple"]
解释: 注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
输出: []
提示:
1 <= s.length <= 201 <= wordDict.length <= 10001 <= wordDict[i].length <= 10s和wordDict[i]仅有小写英文字母组成wordDict中所有字符串都 不同
思路
- 先用
4行代码,将词典构造为前缀树,以word存单词并标识结尾- [dog] => {d: {o : {g : word: 'dog'}}}
- 再用
9行代码,实现递归- 入参:字符串(即
s),开始位置start,初始0 - 遍历字符串,字母不在
前缀树,终止循环- 在
前缀树,再看是不是单词结尾不是,再看下一个字母是,开始位置到当前字母中间的字符串,是不是前缀树的word不是,已跑偏(后面的更不是,不用比了),终止循环是,找到一个单词,开始位置移到下一个字母,递归- 由于
JS是单线程,当前循环会等递归的函数执行完 回溯,即递归函数执行完,循环继续递归相当于深度优先遍历完二叉树的分支
- 由于
- 在
- 边界:当
开始位置超出字符串,停止递归 - 结果放哪里:
- 通常递归有两种方式收集结果:
- 在递归达到边界时,通过
辅助变量收集结果 - 递归
回溯过程中,收集结果。每次返回结果类型与最终结果尽量相同
- 在递归达到边界时,通过
- 预测所有
可能的问题,通常结果是[[],[]...[]]形式,两种方式:- 从上向下
递归过程中,可采用辅助变量作为入参记录每种可能经过的位置 - 从下向上
回溯过程中,边界返回二维数组,遍历二维数组,结果组成新二维数组
- 从上向下
- 通常递归有两种方式收集结果:
- 入参:字符串(即
- 再用
2行代码,实现剪枝- 一次
递归,在循环继续前,总要到达边界。开始位置从0到字符串结尾 - 第
2-n次递归也是如此,不同递归可能经历相同的开始位置(相当于二叉树的结点) - 只要有一个开始位置相等,那么后面的过程都是一样的,不需要重复
- 借
前缀树当哈希表用,以开始位置作索引,记忆回溯结果 - 只要到达
相同开始位置,直接返回前辈们的结果,不需要继续递归
- 一次
- 最后
1行代码,来格式化结果,将二维数组每个数组拼接成字符串
代码
var wordBreak = function(s, wordDict, h = {}) {
wordDict.forEach(w => {
var t = h
for (var v of w) t = t[v] || (t[v] = Object.create(null))
t.word = w
})
var d = (s, start, t = h, r = []) => {
if (h[start]) return h[start]
if (start === s.length) return [[]]
for (var i = start; i < s.length; i++)
if (t = t[s[i]]) {
if (t.word)
if (s.substring(start, start + t.word.length) === t.word)
d(s, i + 1).forEach(w => r.push([t.word].concat(w)))
else break
} else break
return h[start] = r
}
return d(s, 0).map(v => v.join(' '))
};
过程
- 上面的代码经历了4个版本的改造
版本1
- 前缀树 +
递归过程收集结果 + 截取字符串
var wordBreak = function(s, wordDict, h = {}, res = []) {
wordDict.forEach(w => {
var t = h
for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
t.isWord = w
})
var d = (s, r) => {
if (!s) return res.push(r.join(' '))
var t = h
for (var a of s) {
if (t[a]) {
t = t[a]
if (t.isWord) {
var l = t.isWord.length
if (s.substring(0, l) === t.isWord) {
d(s.substring(l), r.concat([t.isWord]))
} else break
}
} else break
}
}
return d(s, []), res
};
版本2
- 前缀树 +
递归过程收集结果 + 截取字符串 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}, res = []) {
if (!canWordBreak(s, wordDict)) return []
wordDict.forEach(w => {
var t = h
for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
t.isWord = w
})
var d = (s, r) => {
if (!s) return res.push(r.join(' '))
var t = h
for (var a of s) {
if (t[a]) {
t = t[a]
if (t.isWord) {
var l = t.isWord.length
if (s.substring(0, l) === t.isWord) {
d(s.substring(l), r.concat([t.isWord]))
} else break
}
} else break
}
}
return d(s, []), res
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
for(var j = 0; ++j <= s.length;)
for(var i = -1; ++i < j;)
if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
return dp[s.length]
};
版本3
- 前缀树 +
递归过程收集结果 + 开始位置索引 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}, res = []) {
if (!canWordBreak(s, wordDict)) return []
wordDict.forEach(w => {
var t = h
for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
t.isWord = w
})
var d = (s, r, start) => {
if (start === s.length) return res.push(r.join(' '))
var t = h
for (var i = start; i < s.length; i++) {
var a = s[i]
if (t[a]) {
t = t[a]
if (t.isWord) {
var l = t.isWord.length
if (s.substring(start, start + l) === t.isWord) {
d(s, r.concat([t.isWord]), i + 1)
} else break
}
} else break
}
}
return d(s, [], 0), res
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
for(var j = 0; ++j <= s.length;)
for(var i = -1; ++i < j;)
if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
return dp[s.length]
};
版本4
- 前缀树 +
递归回溯收集结果 + 开始位置索引 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}) {
if (!canWordBreak(s, wordDict)) return []
wordDict.forEach(w => {
var t = h
for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
t.isWord = w
})
var d = (s, start) => {
if (start === s.length) return [[]]
var t = h, r = []
for (var i = start; i < s.length; i++) {
var a = s[i]
if (t[a]) {
t = t[a]
if (t.isWord) {
var l = t.isWord.length
if (s.substring(start, start + l) === t.isWord) {
for (var w of d(s, i + 1))
r.push([t.isWord].concat(...w))
} else break
}
} else break
}
return r
}
return d(s, 0).map(v=>v.join(' '))
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
for(var j = 0; ++j <= s.length;)
for(var i = -1; ++i < j;)
if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
return dp[s.length]
};