原文链接: https://leetcode-cn.com/problems/distinct-subsequences
英文原文
Given two strings s
and t
, return the number of distinct subsequences of s
which equals t
.
A string's subsequence is a new string formed from the original string by deleting some (can be none) of the characters without disturbing the remaining characters' relative positions. (i.e., "ACE"
is a subsequence of "ABCDE"
while "AEC"
is not).
The test cases are generated so that the answer fits on a 32-bit signed integer.
Example 1:
Input: s = "rabbbit", t = "rabbit" Output: 3 Explanation: As shown below, there are 3 ways you can generate "rabbit" from S.rabbbit
rabbbit
rabbbit
Example 2:
Input: s = "babgbag", t = "bag" Output: 5 Explanation: As shown below, there are 5 ways you can generate "bag" from S.babgbag
babgbag
babgbag
babgbag
babgbag
Constraints:
1 <= s.length, t.length <= 1000
s
andt
consist of English letters.
中文题目
给定一个字符串 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
提示:
0 <= s.length, t.length <= 1000
s
和t
由英文字母组成
通过代码
高赞题解
翻译题意
读懂题目后,用自己的话“翻译”一下题目,有时候会更容易有思路。
题目:求 s 的子序列中 t 出现的个数,blabla…
翻译:在 s 串身上 “挑选” 字符,去匹配 t 串的字符,求挑选的方式数
递归思路
抓住 “选”,s 要照着 t 来挑选,逐字符考察选或不选,分别来到什么状态?
举例,s 为babgbag
,t 为bag
,末尾字符相同,于是 s 有两种选择:
- 用
s[s.length-1]
去匹配掉t[t.length-1]
,问题规模缩小:继续考察babgba
和ba
- 不这么做,但
t[t.length-1]
仍需被匹配,于是在babgba
中继续挑,考察babgba
和bag
是否用它去匹配,是两种不同的挑选方式,各自做下去所产生的方式数,相加,是大问题的解。
现在我们拆解出规模小一点的子问题。完善一下,定义出递归函数:
返回:从开头到s[i]
的子串中,出现『从开头到t[j]
的子串』的次数。
即,从 前者 选字符,去匹配 后者,的方案数。
看了s[i]==t[j]
的情况,那s[i]!=t[j]
的情况呢?s[i]
不匹配t[j]
,唯有拿s[i]
之前的子串去匹配
现在两种情况下的递归公式都好写了。递归树底部的 base case 呢?
随着递归压栈,子问题规模(子串长度)在变小:
- 小到 t 变成空串,此时 s 为了匹配它,方式只有1种:什么字符也不用挑(或 s 也是空串,什么都不做就匹配了,方式数也是1)
- 小到 s 变成空串,但 t 不是,s 怎么也匹配不了 t,方式数为 0
递归函数的参数可以传子串或索引,但用索引描述子问题,不用每次都切割字符串,也更容易迁移到 dp 解法。
未加入记忆化 代码 超时
54 / 62 test cases passed. Status: Time Limit Exceeded
[]//详细注释见下方 记忆化递归 代码 func numDistinct(s string, t string) int { sLen, tLen := len(s), len(t) var helper func(i, j int) int helper = func(i, j int) int { if j < 0 { // base case return 1 } if i < 0 { // 这两个base case 的顺序不能调换!因为 i<0 且 j<0 时 应该返回1 return 0 } if s[i] == t[j] { return helper(i-1, j) + helper(i-1, j-1) } else { return helper(i-1, j) } } return helper(sLen-1, tLen-1) }
[]var numDistinct = function(s, t) { const sLen = s.length, tLen = t.length function helper(i, j) { if (j < 0) { return 1 } if (i < 0) { return 0 } if (s[i] == t[j]) { return helper(i-1, j) + helper(i-1, j-1) } else { return helper(i-1, j) } } return helper(sLen-1, tLen-1) };
加入记忆化 代码
[]func numDistinct(s string, t string) int { sLen, tLen := len(s), len(t) memo := make([][]int, sLen) // 二维memo数组 存储计算过的子问题的结果 for i := range memo { memo[i] = make([]int, tLen) for j := 0; j < tLen; j++ { memo[i][j] = -1 } } var helper func(i, j int) int helper = func(i, j int) int { // 从开头到s[i]的子串中,出现『从开头到t[j]的子串』的 次数 if j < 0 { // base case 当j指针越界,此时t为空串,s不管是不是空串,匹配方式数都是1 return 1 } if i < 0 { // base case i指针越界,此时s为空串,t不是,s怎么也匹配不了t,方式数0 return 0 } if memo[i][j] != -1 { // memo中有当前遇到的子问题的解,直接拿来返回 return memo[i][j] } if s[i] == t[j] { // t[j]被匹配掉,对应helper(i-1, j-1),不被匹配掉对应helper(i-1, j) memo[i][j] = helper(i-1, j) + helper(i-1, j-1) } else { memo[i][j] = helper(i-1, j) // } return memo[i][j] // 返回当前递归子问题的解 } return helper(sLen-1, tLen-1)//从开头到s[sLen-1]的子串中,出现『从开头到t[tLen-1]的子串』的次数 }
[]var numDistinct = function(s, t) { const sLen = s.length, tLen = t.length memo = new Array(sLen) for (let i = 0; i < sLen; i++) { memo[i] = new Array(tLen) for (let j = 0; j < tLen; j++) { memo[i][j] = -1 } } function helper(i, j) { if (j < 0) { return 1 } if (i < 0) { return 0 } if (memo[i][j] != -1) { return memo[i][j] } if (s[i] == t[j]) { memo[i][j] = helper(i-1, j) + helper(i-1, j-1) } else { memo[i][j] = helper(i-1, j) } return memo[i][j] } return helper(sLen-1, tLen-1) };
动态规划 与 递归 的区别
其实我们由下面的递归公式,就看出了递推关系——之前的状态推出了 i,j 的状态
if s[i] == t[j] {
memo[i][j] = helper(i-1, j) + helper(i-1, j-1)
} else {
memo[i][j] = helper(i-1, j)
}
递归是自上而下调用,子问题自下而上被解决,最后解决了整个问题,而dp是从base case 出发,通过在dp数组记录中间结果,自下而上地顺序地解决子问题。
dp就好像一种不带重复计算的递归,想出dp往往也是像想出递归那样,都需要从子问题入手,正确定义子问题,递归想出结束条件,dp想出base case,递归想出递归公式,dp想出递推公式。递归加入记忆化后,往往稍作修改,就是dp的解法。
dp 解法
dp[i][j]
:从开头到s[i-1]
的子串中,出现『从开头到t[j-1]
的子串』的 次数。即:前 i 个字符的 s 子串中,出现前 j 个字符的 t 子串的次数。
状态转移方程:
- 当
s[i-1] != t[j-1]
时,有dp[i][j] = dp[i-1][j]
- 当
s[i-1] == t[j-1]
时,有dp[i][j] = dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
base case
j==0
时,dp[i][0] = 1
i==0
时,dp[0][j] = 0
dp 代码
[]func numDistinct(s string, t string) int { sLen, tLen := len(s), len(t) // dp[i][j]:前 i 个字符的 s 子串中,出现前 j 个字符的 t 子串的次数 dp := make([][]int, sLen+1) // 二维dp数组 for i := range dp { dp[i] = make([]int, tLen+1) } for i := 0; i < sLen+1; i++ { // 遍历dp矩阵,填表 for j := 0; j < tLen+1; j++ { if j == 0 { // base case dp[i][j] = 1 } else if i == 0 { // base case dp[i][j] = 0 } else { // 递推公式 if s[i-1] == t[j-1] { dp[i][j] = dp[i-1][j-1] + dp[i-1][j] } else { dp[i][j] = dp[i-1][j] } } } } return dp[sLen][tLen] // 前 sLen 个字符的 s 串中,出现前 tLen 个字符的 t 串的次数 }
[]var numDistinct = function(s, t) { const sLen = s.length, tLen = t.length dp = new Array(sLen+1) for (let i = 0; i < sLen+1; i++) { dp[i] = new Array(tLen+1).fill(0) } for (let i = 0; i < sLen+1; i++) { for (let j = 0; j < tLen+1; j++) { if (j == 0) { dp[i][j] = 1 } else if (i == 0) { dp[i][j] = 0 } else { if (s[i-1] == t[j-1]) { dp[i][j] = dp[i-1][j-1] + dp[i-1][j] } else { dp[i][j] = dp[i-1][j] } } } } return dp[sLen][tLen] }
复盘总结
把握住:挑选,既然挑选,就会出现对单个字符的不同选择,作出一种选择后,就变成了一个规模小一点的子问题,是我们要计算的状态,我们找到递推关系。
递归自上而下将问题拆解,然后自下而上逐个解决,dp呢,从base case出发,顺序地一个个计算子问题,像填表格一样。
二者的难点都在于定义递归函数(定义dp子问题),有时候不妨先想想递归,加入记忆化,它解决的子问题个数和dp是一样的,本质上记忆化递归的时间复杂度和dp是一样的。
感谢阅读,写作不易,讲解面向新手,话可能比较多,主要侧重在思路,基本涵盖重难点,希望你没带着疑惑离开。点开题解然后带着疑惑关掉,是一种时间的浪费,这感觉我懂。我不想浪费大家时间,所以浪费自己时间(反复读改),写点东西,练练表达。喜欢的点个赞鼓励一下。
这个二维dp可以降维,评论区已经有人贴出来了。
最后修改于 2021-10-08
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
66489 | 127704 | 52.1% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|