学习记录:js算法(一百一十一): 单词拆分 II

单词拆分 II

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

思路一

var wordBreak = function(s, wordDict) {
  // 创建一个空数组来存储所有可能的句子
  const results = [];

  // 将 wordDict 转换为 Set,以便更快地查找单词
  const dictSet = new Set(wordDict);

  /**
   * 回溯函数
   * @param {string} remaining - 剩余未分割的部分
   * @param {string} sentence - 当前构建的句子
   */
  function backtrack(remaining, sentence) {
    // 如果 remaining 为空,说明找到了一个有效的分割
    if (remaining === '') {
      // 将当前构建的句子加入到结果列表中,并移除最后一个空格
      results.push(sentence.slice(0, -1));
      return;
    }

    // 遍历 remaining 的每一个可能的前缀
    for (let i = 1; i <= remaining.length; i++) {
      // 获取一个可能的单词
      const word = remaining.substring(0, i);

      // 如果该单词存在于词典中
      if (dictSet.has(word)) {
        // 递归调用 backtrack 函数,传入剩余的部分和更新后的句子
        backtrack(remaining.substring(i), sentence + word + ' ');
      }
    }
  }

  // 开始回溯过程
  backtrack(s, '');

  // 返回所有可能构建的句子
  return results;
};

讲解
运用回溯的思想,我们可以使用递归来构建所有可能的句子。

  1. 基本情况:
    ○ 如果字符串 s 已经为空,那么说明我们找到了一个有效的分割,将其加入到结果列表中。
    ○ 如果字符串 s 的剩余部分无法通过词典中的单词来构建,那么回溯到上一步。
  2. 递归步骤:
    ○ 从字符串 s 的开头开始尝试匹配词典中的单词。
    ○ 如果匹配成功,则从当前位置之后继续尝试构建句子。
    ○ 如果匹配失败,则回溯到上一步,尝试下一个单词。
  3. 构建句子:
    ○ 在递归的过程中,我们需要构建当前的句子,可以使用一个变量来记录当前构建的句子。
    ○ 每当我们找到一个匹配的单词,就在当前句子后面添加这个单词,并在其后添加一个空格。
  4. 返回结果:
    ○ 最终返回所有可能构建的句子列表。

思路二

var wordBreak = function(s, wordDict) {
  const results = [];
  const dictSet = new Set(wordDict);
  const n = s.length;

  // 动态规划数组,dp[i] 表示从 0 到 i 是否可以被分割
  const dp = new Array(n + 1).fill(false);
  dp[0] = true; // 空字符串可以被分割

  // 记录到达每个位置 i 时所有可能的分割点
  const prev = new Array(n + 1).fill(null);
  prev[0] = []; // 空字符串的分割点为空列表

  // 填充 dp 数组
  for (let i = 1; i <= n; i++) {
    prev[i] = [];
    for (let j = 0; j < i; j++) {
      if (dp[j] && dictSet.has(s.substring(j, i))) {
        dp[i] = true;
        prev[i].push(j);
      }
    }
  }

  // 构建所有可能的句子
  function buildSentence(index, currentSentence) {
    if (index === 0) {
      results.push(currentSentence);
      return;
    }

    for (let prevIndex of prev[index]) {
      const word = s.substring(prevIndex, index);
      buildSentence(prevIndex, word + (currentSentence ? ' ' + currentSentence : ''));
    }
  }

  if (dp[n]) {
    buildSentence(n, '');
  }

  return results;
};

讲解
对于这道题,我们也可以使用一维动态规划(DP)的方法来解决。这种方法可以有效地减少空间复杂度,并且避免了回溯所带来的大量重复计算。

  1. 初始化动态规划数组:
    ○ 创建一个布尔型数组 dp,长度为 n+1,其中 dp[i] 表示字符串 s 的前 i 个字符是否能通过字典中的单词进行分割。
    ○ 初始化 dp[0] 为 true,因为空字符串总是可以被分割。
  2. 记录分割点:
    ○ 创建一个数组 prev,长度同样为 n+1,用于存储到达每一个位置 i 时所有可能的分割点。prev[i] 是一个数组,包含了所有使得 dp[i] 为 true 的前一个分割点。
  3. 填充动态规划数组:
    ○ 对于每一个位置 i 从 1 到 n:
    ■ 遍历从 0 到 i-1 的所有位置 j。
    ■ 如果 dp[j] 为 true 并且子串 s.substring(j, i) 在字典 wordDict 中,则更新 dp[i] 为 true,并且把 j 添加到 prev[i] 中作为有效的分割点。
  4. 构建句子:
    ○ 当 dp[n] 为 true 时,说明整个字符串 s 可以被分割。
    ○ 使用递归函数 buildSentence 来构建所有可能的句子。
    ■ 从最后一个位置 n 开始回溯,对于每一个可能的分割点 prevIndex,取该分割点到当前位置 index 的子串 word,并将这个单词添加到当前句子的开头。
    ■ 当 index 为 0 时,说明已经回溯到了字符串的开始,此时将当前句子添加到结果数组 results 中。
  5. 返回结果:
    ○ 返回所有的可能句子。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值