给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 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
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s和wordDict[i]仅由小写英文字母组成wordDict中的所有字符串 互不相同
1. 生活案例:乐高积木挑战
想象你手里有一根长长的透明塑料管(字符串 s),你的任务是判断能不能用你积木盒里现有的积木块(wordDict)正好把这个塑料管填满。
-
规则:
- 你只能使用积木盒里有的型号。
- 同一种型号的积木你可以无限次使用。
- 所有的积木必须严丝合缝地连接,不能重叠,也不能留空隙。
-
例子:塑料管标着
applepenapple,你的积木盒里有apple和pen。- 你先塞入一个
apple,剩下的空间是penapple。 - 再塞入一个
pen,剩下的空间是apple。 - 最后再塞入一个
apple,刚好填满!返回 True。
- 你先塞入一个
2. 代码解析与“生活化”注释
这段代码的核心逻辑是: “如果前面的部分已经能拼好,且剩下的一截正好是积木盒里的型号,那么到这里为止的全长就都能拼好。”
JavaScript
/**
* @param {string} s - 目标长管子
* @param {string[]} wordDict - 积木盒里的零件
* @return {boolean} - 是否能正好拼满
*/
var wordBreak = function(s, wordDict) {
// 为了找积木更快,把零件清单放进 Set(哈希表),这样一眼就能看出有没有这个型号
let wordList = new Set(wordDict);
let n = s.length;
// dp[i] 代表:塑料管的前 i 个长度,是否能被积木完美拼满
// dp 数组长度是 n+1,因为我们要考虑“长度为 0”的情况
let dp = new Array(n + 1).fill(false);
// 基础情况:长度为 0 时,默认是“拼好了”的(因为不需要任何积木)
dp[0] = true;
// i 代表当前我们正在尝试填满的管子总长度 (1 到 n)
for (let i = 1; i <= n; i++) {
// j 代表寻找一个“切割点”,把管子分成 [0, j] 和 [j, i] 两部分
for (let j = 0; j < i; j++) {
// 生活化解释:
// 1. dp[j] 是 true 吗?(即:前 j 段管子已经拼好了吗?)
// 2. 剩下的这一截 s.substring(j, i) 在积木盒里吗?
if (dp[j] && wordList.has(s.substring(j, i))) {
// 如果两条件都满足,说明到长度 i 为止,管子也能被完美填满
dp[i] = true;
// 既然长度 i 已经能拼好了,就不用再试其他的切割点 j 了,直接跳出内循环
break;
}
}
}
// 最后看整根管子的长度 n 是否标记为 true
return dp[n];
};
3. 为什么代码这样写?(算法思维)
-
分步检查:我们不是直接去看整根管子,而是从长度 1 开始看,长度 2、长度 3…… 每一个长度都基于之前的成功经验。
-
状态转移:
- 如果我们想知道长度为 10 的管子能不能拼好,我们会问:
- “长度为 7 的能拼好吗?(
dp[7])且最后那 3 个字母的积木我有吗?” - 或者“长度为 5 的能拼好吗?(
dp[5])且最后那 5 个字母的积木我有吗?”
-
效率优化:代码里的
break很关键。一旦发现长度i可以被某种方式拼好,就立刻处理下一个长度,不再浪费时间尝试其他拼法。
总结
这就是动态规划的魅力:不重复造轮子。为了知道现在能不能成功,先看看过去(更短的长度)有没有成功的记录,再接上现在的这一块积木。