动态规划 - 139. 单词拆分

117 阅读2分钟

4月日新计划更文活动 第18天

前言

动态规划专题,从简到难通关动态规划。

每日一题

今天的题目是 139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 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 <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅有小写英文字母组成
  • wordDict 中的所有字符串 互不相同

题解

回溯算法

  1. 划分子串 对于字符串s,我们可以在任意位置对其进行划分,得到一个新的字串。例如:“leetcode”,可以得到新的字串"leet"和"code"。
  2. 检查拆分 对于划分得到的字串,我们需要检查其是否可以由wordDict中的单词组合而成。如果可以进行拆分划分,直到整个字符串都可以被拆分成由单词组成的形式。如果不能进行拆分,则回溯到上一次进行拆分的位置,并重新进行拆分的处理操作。
  3. 可以使用了memory数组来记录每个startIndex位置开始的子串是否可以被拆分成符合要求的形式。当发现当前子串被处理过时,直接使用已知结果,避免重复处理同一个子问题,减少计算量。
function wordBreak(s: string, wordDict: string[]): boolean {
  const wordSet = new Set(wordDict);
  const memory = new Array(s.length).fill(-1); 

  function backtracking(startIndex) {
    if (startIndex >= s.length) {
      return true;
    }
    
    if (memory[startIndex] !== -1) {
      return memory[startIndex] === 1;
    }
    
    for (let i = startIndex; i < s.length; i++) {
      const word = s.substring(startIndex, i + 1);
      if (wordSet.has(word) && backtracking(i + 1)) {
        memory[startIndex] = 1;
        return true;
      }
    }
    
    memory[startIndex] = 0;
    return false;
  }
  
  return backtracking(0);
}

image.png

动态规划

  1. 确定dp数组以及下标的含义

我们可以定义 dp[i] 表示字符串 si 个字符是否可以由字典中的单词拼接而成。由于字典中的单词可以重复使用,所以这是一个完全背包问题。

  1. 确定递推公式

对于每个i,我们要枚举从0到i-1的j值,表示将字符串s分为左右两部分,前面的j位和后面的i-j位。如果前面的j位可以被单词字典中的某个单词匹配,并且dp[j]为true,则说明前i位可以被完全匹配。因此有以下递推公式:

dp[i] = dp[j] && wordSet.has(s.substring(j, i));
  1. dp数组如何初始化

由于s为空字符串时,它一定可以被单词字典中的单词匹配,因此有dp[0] = true。其余位置的默认值为false,因为我们需要将它们递推得到。

  1. 确定遍历顺序

外层循环遍历字符串s的每个位置,即从1到s.length。内层循环遍历从0到i-1的每个值,表示将字符串s分为左右两部分。由于我们需要判断前面的j位是否可以被分解成字典中的单词,因此内层循环需要在外层循环之前遍历。

  1. 举例推导dp[i]

0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 T F F F T F F F T

i012345678
dp[i]TFFFTFFFT

代码:

function wordBreak(s: string, wordDict: string[]): boolean {
    const wordSet = new Set(wordDict);
    const dp = new Array(s.length + 1).fill(false);
    dp[0] = true;
    for (let i = 1; i <= s.length; i++) {
        for (let j = 0; j < i; j++) {
            const word = s.substr(j, i - j);
            if (wordSet.has(word) && dp[j]) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.length];
}

image.png