1. 题目描述
给你一个字符串 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
2. 动态规划解法
const wordBreak = (s, wordDict) => {
// 初始化
let dp = new Array(s.length + 1).fill(false)
dp[0] = true
// 遍历顺序,先背包后物品,需要有顺序
for (let i = 0; i <= s.length; i++) {
for (let j = 0; j < wordDict.length; j++) {
if (i - wordDict[j].length >= 0) {
if (s.slice(i - wordDict[j].length, i) === wordDict[j] && dp[i - wordDict[j].length]) {
dp[i] = true
}
}
}
}
return dp[s.length]
}
console.log(wordBreak("applepenapple", ["apple", "pen"]));
时间复杂度:O(n * m) n是s字符串长度,m是wordDict字典长度
空间复杂度:O(n) n是s字符串长度
分析:
-
dp[i]的含义,表示长度为i的字符串,可以拆分为一个或者多个在字典中出现的子串
-
递推公式:
- if [i - wordDict[j].length, i)之间的字符串和wordDict[j]相等 && dp[i - wordDict[j].length]值是true,则dp[i] 可以改为true,这里的i是背包,j是字典里面的字符串
- 这个递推公式和之前的都不太一样,[i - wordDict[j].length, i)其实就是从 s里面截取字符串,看看和字典里面的是不是相等的
- dp[i - wordDict[j].length]为什么必须为true?比如applepenapple,必须是[0, 5)的apple匹配成功,那么dp[5]就是true,才能接着去匹配后面的pen,pen成功,接着又去匹配后面的apple,这样最终才是true,仅仅只是中途的pen匹配上了,不能算是成功
-
初始化
- 一维dp,初始化为s的长度,均为false,但是dp[0]为true,为了方便后续的初始化,也可以这样理解:字符串长度为0的字符串就是'',空字符在字典里面的任意子串都可以拆分出来
-
遍历顺序
- 先遍历背包,再遍历物品。s就是背包,字典里面的字符串就是物品
- 因为这里物品放入背包需要讲究顺序,applepenapple,就必须先放入apple再放pen再放apple;当顺序很重要时,就得先遍历背包
-
举例推导
测试用例为:"applepenapple", ["apple", "pen"]
如下三种情况时,dp[i]为true,否则均为false
当 i = 5, j = 5 时,此时 s 字符串截取 [0, 5),apple 正好和 dp[0] 相等,且 dp[i - wordDict[0].length] = dp[5 - 5] = true 则把 dp[5] 改为 true
当 i = 8, j = 1 时,此时 s 字符串截取 [5, 8),pen 正好和 dp[1] 相等,&& dp[i - wordDict[0].length] = dp[8 - 3] = dp[5] = true 则把 dp[8] 改为 true
当 i = 13, j = 0 时,此时 s 字符串截取 [8, 13),apple 正好和 dp[0] 相等,&& dp[i - wordDict[0].length] = dp[13 - 5] = dp[8] = true 则把 dp[13] = true
3. 回溯写法-剪枝(不会超时)
var wordBreak = function(s, wordDict) {
let memo = {}
let set = new Set(wordDict) // 这里可以不用set,题目描述说了字典里面的单词互不相同
// 递归函数
function backtrack (start) {
// 成功退出条件
if (start === s.length) return true
// 剪枝
if (start in memo) return memo[start]
for (const word of set) {
// 从'' 拼接上来leet,看leet是否和'leetcode'的前面部分是匹配的
if (isMatch(s, start, word)) {
if (backtrack(start + word.length)) {
// 意味着从start到字符串末尾可以拆分
memo[start] = true
return true
}
}
}
// 意味着从start到字符串末尾不能拆分
memo[start] = false
return false
}
// 从s的start开始,比较每个字符是否和word的相等,是才继续
function isMatch(s, start, word) {
// 如果长度超出,return
if (start + word.length > s.length) return false
// 每个字符都要比较
for (let i = 0; i < word.length; i++) {
if (s[start + i] !== word[i]) return false
}
return true
}
return backtrack(0)
};
时间复杂度: O(m * n * k), m是字典长度,n是s字符的长度,k是字典里面单词的平均长度。因为有记忆化,所以每个起始位置只会进行一次匹配,否则时间复杂度肯呢个高达,m ^ n次
空间复杂度:O(n)
分析: 测试用例: wordBreak("applepenapple", ["apple", "pen"]
1. start = 0, isMatch(s, 0, 'apple') 会 return true
2. 回溯,start = 0 + word.length = 5
3. start = 5, isMatch(s, 5, 'apple') 会 return false
4. start = 5, isMatch(s, 5, 'pen') 会 return true
5. 回溯,start = 5 + pen.length = 8
6. start = 8, isMatch(s, 8, 'apple') return true, 这里就成了
7. 把 memo[8] = true,
8. 把 memo[5] = true
9. 把 memo[0] = true