单词拆分 II
给定一个字符串 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"]
输出:[]
思路一
var wordBreak = function(s, wordDict) {
// 创建一个空数组来存储所有可能的句子
const results = [];
// 将 wordDict 转换为 Set,以便更快地查找单词
const dictSet = new Set(wordDict);
/**
* 回溯函数
* @param {string} remaining - 剩余未分割的部分
* @param {string} sentence - 当前构建的句子
*/
function backtrack(remaining, sentence) {
// 如果 remaining 为空,说明找到了一个有效的分割
if (remaining === '') {
// 将当前构建的句子加入到结果列表中,并移除最后一个空格
results.push(sentence.slice(0, -1));
return;
}
// 遍历 remaining 的每一个可能的前缀
for (let i = 1; i <= remaining.length; i++) {
// 获取一个可能的单词
const word = remaining.substring(0, i);
// 如果该单词存在于词典中
if (dictSet.has(word)) {
// 递归调用 backtrack 函数,传入剩余的部分和更新后的句子
backtrack(remaining.substring(i), sentence + word + ' ');
}
}
}
// 开始回溯过程
backtrack(s, '');
// 返回所有可能构建的句子
return results;
};
讲解
运用回溯的思想,我们可以使用递归来构建所有可能的句子。
- 基本情况:
○ 如果字符串 s 已经为空,那么说明我们找到了一个有效的分割,将其加入到结果列表中。
○ 如果字符串 s 的剩余部分无法通过词典中的单词来构建,那么回溯到上一步。- 递归步骤:
○ 从字符串 s 的开头开始尝试匹配词典中的单词。
○ 如果匹配成功,则从当前位置之后继续尝试构建句子。
○ 如果匹配失败,则回溯到上一步,尝试下一个单词。- 构建句子:
○ 在递归的过程中,我们需要构建当前的句子,可以使用一个变量来记录当前构建的句子。
○ 每当我们找到一个匹配的单词,就在当前句子后面添加这个单词,并在其后添加一个空格。- 返回结果:
○ 最终返回所有可能构建的句子列表。
思路二
var wordBreak = function(s, wordDict) {
const results = [];
const dictSet = new Set(wordDict);
const n = s.length;
// 动态规划数组,dp[i] 表示从 0 到 i 是否可以被分割
const dp = new Array(n + 1).fill(false);
dp[0] = true; // 空字符串可以被分割
// 记录到达每个位置 i 时所有可能的分割点
const prev = new Array(n + 1).fill(null);
prev[0] = []; // 空字符串的分割点为空列表
// 填充 dp 数组
for (let i = 1; i <= n; i++) {
prev[i] = [];
for (let j = 0; j < i; j++) {
if (dp[j] && dictSet.has(s.substring(j, i))) {
dp[i] = true;
prev[i].push(j);
}
}
}
// 构建所有可能的句子
function buildSentence(index, currentSentence) {
if (index === 0) {
results.push(currentSentence);
return;
}
for (let prevIndex of prev[index]) {
const word = s.substring(prevIndex, index);
buildSentence(prevIndex, word + (currentSentence ? ' ' + currentSentence : ''));
}
}
if (dp[n]) {
buildSentence(n, '');
}
return results;
};
讲解
对于这道题,我们也可以使用一维动态规划(DP)的方法来解决。这种方法可以有效地减少空间复杂度,并且避免了回溯所带来的大量重复计算。
- 初始化动态规划数组:
○ 创建一个布尔型数组 dp,长度为 n+1,其中 dp[i] 表示字符串 s 的前 i 个字符是否能通过字典中的单词进行分割。
○ 初始化 dp[0] 为 true,因为空字符串总是可以被分割。- 记录分割点:
○ 创建一个数组 prev,长度同样为 n+1,用于存储到达每一个位置 i 时所有可能的分割点。prev[i] 是一个数组,包含了所有使得 dp[i] 为 true 的前一个分割点。- 填充动态规划数组:
○ 对于每一个位置 i 从 1 到 n:
■ 遍历从 0 到 i-1 的所有位置 j。
■ 如果 dp[j] 为 true 并且子串 s.substring(j, i) 在字典 wordDict 中,则更新 dp[i] 为 true,并且把 j 添加到 prev[i] 中作为有效的分割点。- 构建句子:
○ 当 dp[n] 为 true 时,说明整个字符串 s 可以被分割。
○ 使用递归函数 buildSentence 来构建所有可能的句子。
■ 从最后一个位置 n 开始回溯,对于每一个可能的分割点 prevIndex,取该分割点到当前位置 index 的子串 word,并将这个单词添加到当前句子的开头。
■ 当 index 为 0 时,说明已经回溯到了字符串的开始,此时将当前句子添加到结果数组 results 中。- 返回结果:
○ 返回所有的可能句子。