【动态规划】【记忆化回溯】LeetCode139. 单词拆分

297 阅读2分钟

题目

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明:

1.拆分时可以重复使用字典中的单词。

2.你可以假设字典中没有重复的单词。

示例 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. 定义状态
2. 初始状态
3. 状态转移方程
4. 从dp[]中获取结果

具体到题目

定义状态

定义数组dp[i]表示字符s的0到i-1位置的子字符串是否能由wordDict中的单词组成

初始状态

dp[0]表示的是空字符""能否由wordDict中的单词组成,默认是成立的,因此dp[0]=true

状态转移方程

dp[i] 需要遍历wordDict每个单词word,根据word的长度p一起获得,以示例 1为例: 当i=4时,遍从dp[]中获取结果历wordDict的第一个元素leet,得到p=4,因此p[4] = p[0]+ s.substring(4-4,4) === 'leet'。

从dp[]中获取结果

dp[]的最后一项就是最终结果

动态规划完整代码

var wordBreak = function (s, wordDict) {
  const len = s.length;
  const dp = new Array(len + 1).fill(false);
  dp[0] = true;
  for (let i = 1; i < len + 1; i++) {
    for (let j = 0; j < wordDict.length; j++) {
      if (dp[i] === true) {
        break;
      }
      const cmplen = wordDict[j].length;
      if (cmplen > i) {
        continue;
      }
      const str = s.substring(i - cmplen, i);
      console.log(str);
      dp[i] = dp[i - cmplen] && str === wordDict[j];
    }
  }
  console.log(dp);
  return dp.pop();
};

记忆化回溯

刚开始的时候不知道怎么根据动态规划建模,所以看完题目的第一个想法就是回溯。 刚开始的代码如下

var wordBreak = function (s, wordDict) {
  const res = false;
  const backtrack = (string) => {
    if (string === '') {
      res = true;
      return;
    }
    for (let i = 0; i < wordDict.length; i++) {
      const helpStr = string.substring(index);
      if (helpStr.startsWith(wordDict[i])) {
        const match = wordDict[i];
        const matchLen = match.length;
        backtrack(s.substring(matchLen));
    }
  };
  return backtrack(s, 0);
};

想法思路是通过不断是裁剪字符串s,使得最后得到空字符串"",结果就是true。示例能跑过,但是碰到

s =
  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab';
wordDict = [
  'a',
  'aa',
  'aaa',
  'aaaa',
  'aaaaa',
  'aaaaaa',
  'aaaaaaa',
  'aaaaaaaa',
  'aaaaaaaaa',
  'aaaaaaaaaa'
];

就超时了。

分析原因就是当第一个a计算了两次后的结果其实和aa计算第一次的结果是重复的,即如果i位置已经计算过了,那么结果不需要再重复计算一遍。所以需要引入记忆化数组dp来记录i位置是否已经计算过并且能否满足前i个位置组成的字符串能否被wordDict拆分。

记忆化回溯完整代码

var wordBreak = function (s, wordDict) {
  const dp = new Array(len).fill(false);
  const backtrack = (string, index) => {
    if (index === string.length) {
      return true;
    }
    if (dp[index]) {
      return false;
    }
    for (let i = 0; i < wordDict.length; i++) {
      const helpStr = string.substring(index);
      if (helpStr.startsWith(wordDict[i])) {
        const match = wordDict[i];
        const matchLen = match.length;
        if (backtrack(s, index + matchLen)) {
          return true;
        }
      }
    }
    dp[index] = true;
    return false;
  };
  return backtrack(s, 0);
};

总结

本道题目顺便复习了一下回溯算法的基本思路,即

result = [];
def backtrack(path, list)
   if(满足条件){
   	 result.push(path)
   }
   for 选择 in list
   	 做选择
     backtrack(path, list)
     撤销选择

本道题目动态规划的难点我感觉是建模,就是理解用数组dp表示前i个字符串能否由wordDict拆分成功。需要多总结经验