【动态规划】LeetCode 115 不同的子序列-Hard

541 阅读2分钟

题目

定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^

动态规划四步走

1. 定义状态
2. 初始状态
3. 状态转移方程
4. 从dp[]中获取结果

具体到题目

定义状态

此处需要定义二维数组dp表示s前i个元素构成的子序列中包含t中前j个元素的构成的子序列的所有可能

初始状态

dp二维数组中i=0和j=0表示的是空字符串,即"",默认""是其他所有子序列的子序列,因此dp数组的第一行均为1,即dp[0] = [1,1,...,1]

状态转移方程

当i和j指针指向的字符不同时,譬如示例1中j指向的是第一个rabbbit中的rabb位置,而i指向的是rabbit的ra位置,此时的情况表示的是rabb中包含ra的所有情况,与rab中包含ra的情况相同,因此dp[i][j] = dp[i][j-1]。

当i和j指针指向的字符相同时,譬如示例1中j指向的是第一个rabbbit中的rabb位置,而i指向的是rabbit的rab位置,此时的情况表示的是rabb中包含rab的所有情况,此时的结果等于rab中包含ra的情况与rab中包含rab的情况之和,即dp[i][j] = dp[i][j-1]+dp[i-1][j-1];

因此得到状态转移方程为:

if t[i] !== s[j] => dp[i][j] = dp[i][j-1]
else dp[i][j] = dp[i][j-1]+dp[i-1][j-1]

从dp[]中获取结果

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

完整代码

/**
 * @param {string} s
 * @param {string} t
 * @return {number}
 */
var numDistinct = function (s, t) {
  const sList = s.split('');
  const tList = t.split('');
  if (tList.length > sList.length) {
    return 0;
  }
  const m = tList.length + 1;
  const n = sList.length + 1;
  const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
  const first = new Array(n);
  first.fill(1);
  dp[0] = first;
  for (let i = 0; i < tList.length; i++) {
    const a = tList[i];
    for (let j = 0; j < sList.length; j++) {
      const b = sList[j];
      if (a === b) {
        dp[i + 1][j + 1] = dp[i + 1][j] + dp[i][j];
      } else {
        dp[i + 1][j + 1] = dp[i + 1][j];
      }
    }
  }
  return dp[m - 1][n - 1];
};

总结

遇到这种复杂的情况,需要增加维度来分析,此处就是建立的二维的dp数组。

在js建立二维数组并设置默认值时,可以通过new Array(m).fill(0).map(()=> new Array(n).fill(0))实现

本题的难点还是在于状态转移方程的寻找,特别需要注意的是理解其关系。