动态规划-单词拆分,完全背包,JavaScript写法,

58 阅读4分钟

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