题目
定一个字符串 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))实现
本题的难点还是在于状态转移方程的寻找,特别需要注意的是理解其关系。