「前端刷题」140.单词拆分 II(HARD)

196 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情

题目(Word Break II)

链接:https://leetcode-cn.com/problems/word-break-ii
解决数:719
通过率:52.4%
标签:字典树 记忆化搜索 哈希表 字符串 动态规划 回溯 
相关公司:amazon facebook google 

给定一个字符串 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"]
输出: []

 

提示:

  • 1 <= s.length <= 20
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 10
  • s 和 wordDict[i] 仅有小写英文字母组成
  • wordDict 中所有字符串都 不同

思路

  • 先用4行代码,将词典构造为前缀树,以word存单词并标识结尾
    • [dog] => {d: {o : {g : word: 'dog'}}}
  • 再用9行代码,实现递归
    • 入参:字符串(即s),开始位置start,初始0
    • 遍历字符串,字母不在前缀树,终止循环
      • 前缀树,再看是不是单词结尾
        • 不是,再看下一个字母
        • ,开始位置到当前字母中间的字符串,是不是前缀树的word
          • 不是,已跑偏(后面的更不是,不用比了),终止循环
          • ,找到一个单词,开始位置移到下一个字母,递归
            • 由于JS是单线程,当前循环会等递归的函数执行完
            • 回溯,即递归函数执行完,循环继续
            • 递归相当于深度优先遍历完二叉树的分支
    • 边界:当开始位置超出字符串,停止递归
    • 结果放哪里:
      • 通常递归有两种方式收集结果:
        • 在递归达到边界时,通过辅助变量收集结果
        • 递归回溯过程中,收集结果。每次返回结果类型与最终结果尽量相同
      • 预测所有可能的问题,通常结果是[[],[]...[]]形式,两种方式:
        • 从上向下递归过程中,可采用辅助变量作为入参记录每种可能经过的位置
        • 从下向上回溯过程中,边界返回二维数组,遍历二维数组,结果组成新二维数组
  • 再用2行代码,实现剪枝
    • 一次递归,在循环继续前,总要到达边界。开始位置从0字符串结尾
    • 2-n递归也是如此,不同递归可能经历相同的开始位置(相当于二叉树的结点)
    • 只要有一个开始位置相等,那么后面的过程都是一样的,不需要重复
    • 前缀树哈希表用,以开始位置作索引,记忆回溯结果
    • 只要到达相同开始位置,直接返回前辈们的结果,不需要继续递归
  • 最后1行代码,来格式化结果,将二维数组每个数组拼接成字符串

代码

var wordBreak = function(s, wordDict, h = {}) {
    wordDict.forEach(w => {
        var t = h
        for (var v of w) t = t[v] || (t[v] = Object.create(null))
        t.word = w
    })
    var d = (s, start, t = h, r = []) => {
        if (h[start]) return h[start]
        if (start === s.length) return [[]]
        for (var i = start; i < s.length; i++)
            if (t = t[s[i]]) {
                if (t.word) 
                    if (s.substring(start, start + t.word.length) === t.word) 
                        d(s, i + 1).forEach(w => r.push([t.word].concat(w)))
                    else break
            } else break
        return h[start] = r
    }
    return d(s, 0).map(v => v.join(' '))
};

过程

  • 上面的代码经历了4个版本的改造

版本1

  • 前缀树 + 递归过程收集结果 + 截取字符串
var wordBreak = function(s, wordDict, h = {}, res = []) {
    wordDict.forEach(w => {
        var t = h
        for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
        t.isWord = w
    })
    var d = (s, r) => {
        if (!s) return res.push(r.join(' '))
        var t = h
        for (var a of s) {
            if (t[a]) {
                t = t[a]
                if (t.isWord) {
                    var l = t.isWord.length
                    if (s.substring(0, l) === t.isWord) {
                        d(s.substring(l), r.concat([t.isWord]))
                    } else break
                }
            } else break
        }
    }
    return d(s, []), res
};

版本2

  • 前缀树 + 递归过程收集结果 + 截取字符串 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}, res = []) {
    if (!canWordBreak(s, wordDict)) return []
    wordDict.forEach(w => {
        var t = h
        for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
        t.isWord = w
    })
    var d = (s, r) => {
        if (!s) return res.push(r.join(' '))
        var t = h
        for (var a of s) {
            if (t[a]) {
                t = t[a]
                if (t.isWord) {
                    var l = t.isWord.length
                    if (s.substring(0, l) === t.isWord) {
                        d(s.substring(l), r.concat([t.isWord]))
                    } else break
                }
            } else break
        }
    }
    return d(s, []), res
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
   for(var j = 0; ++j <= s.length;)
      for(var i = -1; ++i < j;) 
        if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
    return dp[s.length]
};

版本3

  • 前缀树 + 递归过程收集结果 + 开始位置索引 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}, res = []) {
    if (!canWordBreak(s, wordDict)) return []
    wordDict.forEach(w => {
        var t = h
        for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
        t.isWord = w
    })
    var d = (s, r, start) => {
        if (start === s.length) return res.push(r.join(' '))
        var t = h
        for (var i = start; i < s.length; i++) {
            var a = s[i]
            if (t[a]) {
                t = t[a]
                if (t.isWord) {
                    var l = t.isWord.length
                    if (s.substring(start, start + l) === t.isWord) {
                        d(s, r.concat([t.isWord]), i + 1)
                    } else break
                }
            } else break
        }
    }
    return d(s, [], 0), res
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
   for(var j = 0; ++j <= s.length;)
      for(var i = -1; ++i < j;) 
        if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
    return dp[s.length]
};

版本4

  • 前缀树 + 递归回溯收集结果 + 开始位置索引 + 动态规划判断能否拆分
var wordBreak = function(s, wordDict, h = {}) {
    if (!canWordBreak(s, wordDict)) return []
    wordDict.forEach(w => {
        var t = h
        for (var v of w) !t[v] && (t[v] = Object.create(null)), t = t[v]
        t.isWord = w
    })
    var d = (s, start) => {
        if (start === s.length) return [[]]
        var t = h, r = []
        for (var i = start; i < s.length; i++) {
            var a = s[i]
            if (t[a]) {
                t = t[a]
                if (t.isWord) {
                    var l = t.isWord.length
                    if (s.substring(start, start + l) === t.isWord) {
                        for (var w of d(s, i + 1))
                        r.push([t.isWord].concat(...w))
                    } else break
                }
            } else break
        }
        return r
    }
    return d(s, 0).map(v=>v.join(' '))
};
var canWordBreak = (s, wordDict, w = new Set(wordDict), dp = [true]) => {
   for(var j = 0; ++j <= s.length;)
      for(var i = -1; ++i < j;) 
        if (dp[j] = dp[i] & w.has(s.substring(i, j))) break
    return dp[s.length]
};