题目难度: 困难
原题链接
今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复
剑指offer2
就能看到该系列当前连载的所有文章了, 记得关注哦~
题目描述
给定一个字符串 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 由英文字母组成
题目思考
- t 要想成为 s 的子序列, 要满足什么条件? 是否可以从单个字符的角度出发?
解决方案
- 分析题目, 要想求在 s 的子序列中 t 出现的个数, 我们先来考虑各自的最后一个字符
s[-1]
和t[-1]
, 不难发现有三种情况:s[-1]
等于t[-1]
s[-1]
不等于t[-1]
- 对于情况 1, 有两种选择:
- 使用
t[-1]
, 需要继续判断s[-2]
是否与t[-2]
相等; - 不使用
t[-1]
, 需要继续判断s[-2]
是否与t[-1]
相等
- 使用
- 对于情况 2, 只有一种选择:
- 无法使用
t[-1]
, 所以需要继续判断s[-2]
是否与t[-1]
相等
- 无法使用
- 不难发现当前结果和前面的结果存在转移关系, 可以尝试用动态规划来解决
- 我们维护 s 的前 i 个字符的所有子序列中, t 的前 j 个字符出现的次数, 那么上面的情况 1 就可以从
(i-1,j)
和(i-1,j-1)
下标对转移而来, 而情况 2 则可以从(i-1,j)
下标对转移而来, 这就是典型的动态规划的思想 - 用数学语言来表示: 假设
dp[i][j]
代表以 s 的前 i 个字符的所有子序列中, t 的前 j 个字符出现的次数, 那么就有:if s[i-1]==t[j-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
else:
dp[i][j] = dp[i-1][j]
- 最终结果就是
dp[len(s1)][len(s2)]
- 注意当 j 为 0 时, 其 dp 值为 1, 因为总是可以包含空串; 而当该条件不满足且 i 为 0 时, 其 dp 值为 0, 因为空串不可能存在非空子序列
- 下面的代码中使用记忆化搜索实现, 更加直观, 而且有详细的注释, 方便大家理解
复杂度
- 时间复杂度
O(MN)
: 假设 s 和 t 的长度分别是 M 和 N, 需要两重循环求 DP 值 - 空间复杂度
O(MN)
: 二维记忆化搜索的空间消耗
代码
class Solution:
def numDistinct(self, s: str, t: str) -> int:
# 记忆化搜索, dp(i,j)表示s的前i个字符的所有子序列中, t的前j个字符出现的次数, 结果就是dp(len(s),len(t))
@functools.cache
def dp(i, j):
if j == 0:
# 当j为0时, 返回1, 因为总是可以包含空串
return 1
if i == 0:
# 当上面条件不满足且i为0时, 返回0, 因为空串不可能存在非空子序列
return 0
# 两种情况都可以从(i-1,j)转移而来
res = dp(i - 1, j)
if s[i - 1] == t[j - 1]:
# 当末尾两字符相等时, 还可以从(i-1,j)转移而来, 累加其计数
res += dp(i - 1, j - 1)
return res
return dp(len(s), len(t))
大家可以在下面这些地方找到我~😊
我的 GitHub
我的 Leetcode
我的 CSDN
我的知乎专栏
我的头条号
我的牛客网博客
我的公众号: 算法精选, 欢迎大家扫码关注~😊