140. 单词拆分 II
题目描述
给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。
注意:词典中的同一个单词可能在分段中被重复使用多次。
解题思路
思路一:回溯算法
题目如果是「一个问题的所有的具体解」,一般而言使用回溯算法完成
1. 状态定义
定义 dp[i] 表示「长度」为 i 的 s 前缀子串可以拆分成 wordDict 中的单词
2. 确定递推公式
我们用指针j去分割, 看s[0…j - 1]组成的字符串s1和s[j…i - 1]组成的字符串s2是否都是单词表的单词,因为我们已经知道dp[j]的值, 剩下的我们只需要看s2是否是单词表的单词组成即可, 因此我们可以得出以下转移方程: dp[i] = dp[j] && check(s[i… i - 1]); 其中check(s[i… i - 1])表示子串s[i… i - 1]是否出现在字典中。
3. 初始状态
长度包括0, 因此状态数组的长度为len + 1, 0 这个值需要被后面的状态值参考,如果一个单词正好在 wordDict 中,dp[0] 设置成 true 是合理的, 因此dp[0] = true;
4. 遍历属性
j是在[0,i]之间,所以遍历i的for循环一定在外层,这里遍历j的for循环在内层才能通过 计算过的dp[j]数值推导出dp[i]。
5. 输出
返回结果res;
实现代码如下:
/**
* @param {string} s
* @param {string[]} wordDict
* @return {string[]}
*/
var wordBreak = function(s, wordDict) {
let len = s.length,
// 方便判断
wordDictSet = new Set(wordDict),
// 第 1 步:动态规划计算是否有解
// dp[i] 表示「长度」为 i 的 s 前缀子串可以拆分成 wordDict 中的单词
// 长度包括 0 ,因此状态数组的长度为 len + 1
dp = Array(len + 1).fill(false);
// 0 这个值需要被后面的状态值参考,如果一个单词正好在 wordDict 中,dp[0] 设置成 true 是合理的
dp[0] = true;
for (let right = 1; right <= len; right++) {
// 如果单词集合中的单词长度都不长,从后向前遍历是更快的
for (let left = right - 1; left >= 0; left--) {
// substring 不截取 s[right],dp[left] 的结果不包含 s[left]
if (wordDictSet.has(s.substring(left, right)) && dp[left]) {
dp[right] = true;
// 这个 break 很重要,一旦得到 dp[right] = true ,不必再计算下去
break;
}
}
}
// 第 2 步:回溯算法搜索所有符合条件的解
let res = [];
if (dp[len]) {
let path = [];
dfs(s, len, wordDictSet, dp, path, res);
return res;
}
return res;
};
// s[0:len) 如果可以拆分成 wordSet 中的单词,把递归求解的结果加入 res 中
const dfs = function (s, len, wordDictSet, dp, path, res) {
if (len == 0) {
res.push(path.join(" "));
return;
}
// 可以拆分的左边界从 len - 1 依次枚举到 0
for (let i = len - 1; i >= 0; i--) {
let suffix = s.substring(i, len);
if (wordDictSet.has(suffix) && dp[i]) {
path.unshift(suffix);
dfs(s, i, wordDictSet, dp, path, res);
path.shift();
}
}
}