题目
给定一个字符串 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
分析
之前写过时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m ) O(m) O(m)的DP解法,现在回过头来看反而花了很长时间才看懂,因此这里记录一下。
首先很明确能看出来这是一道动态规划可解的题目,问题就是状态转移方程怎么写。在写方程之前我们手动计算下如何进行求解。因为题目要求的是子序列而不是子串,因此从头到尾遍历s,保存当前轮次下s子序列中关于字符串t的前缀,遍历完之后等于t的那个前缀的个数即答案,具体如下所示:
以示例2为例子,从头到尾遍历s,并保存相应的前缀。
- s[0] == “b”,当前s子序列中关于t的前缀为 “b” = 1
- s[1] == “a”,相应前缀个数为 “b” = 1, “ba” = 1
- s[2] == “b”,相应前缀个数为 “b” = 2 “ba” = 1
- s[3] == “g”,相应前缀个数为 “b” = 2 “ba” = 1, “bag” = 1
- s[4] == “b”,相应前缀个数为 “b” = 3 “ba” = 1, “bag” = 1
- s[5] == “a”,相应前缀个数为 “b” = 3 “ba” = 4, “bag” = 1
- s[6] == “g”,相应前缀个数为 “b” = 3 “ba” = 4, “bag” = 5
最终"bag"的个数即为答案5。通过这个过程我们可以发现,如果s的字符x等于t中的字符y,那么以y为结尾的前缀个数为上一轮中以y为结尾的前缀数和以y前一个字符为结尾的前缀数相加。比如,在上述例子的第6步,s[5] == “a”,那么这一轮"ba"的个数相当于s上一轮中“ba”的个数加上“b”的个数。如果s的字符不等于任何t的字符,那么所有前缀的个数保持不变。
根据得到的结论可以写出状态转移方程。用dp[i][j]
表示s中前i个字符中存在的以t[j]为结尾的前缀个数,可知状态转移方程如下所示
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
−
1
]
+
d
p
[
i
−
1
]
[
j
]
,
i
f
s
[
i
]
=
=
t
[
j
]
d
p
[
i
−
1
]
[
j
]
,
i
f
s
[
i
]
!
=
t
[
j
]
dp[i][j] = \left\{ \begin{aligned} &dp[i-1][j-1] + dp[i-1][j], &if {\quad} s[i] == t[j] \\ &dp[i-1][j], &if {\quad} s[i]!= t[j] \end{aligned} \right.
dp[i][j]={dp[i−1][j−1]+dp[i−1][j],dp[i−1][j],ifs[i]==t[j]ifs[i]!=t[j]
注意j=0时,t相当于空字符串,s任意子串都包含t,因此dp[i][0] = 1
代码(golang)
func numDistinct(s string, t string) int {
dp := make([][]int, len(s) + 1)
for i := range dp {
dp[i] = make([]int, len(t) + 1)
dp[i][0] = 1
}
for i := 1; i < len(dp); i++ {
for j := 1; j < len(dp[0]); j++ {
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[len(s)][len(t)]
}
空间复杂度优化
观察到上述遍历过程中,每一轮更新只用到上一轮的结果,即第i次遍历只用第i-1次遍历时的结果。因此可以只保存两轮的状态值,这是优化的一个简单思路,此处不做赘述。
我的思路是dp[i][j]
更新时是dp[i-1][j-1]+dp[i-1][j]
,如果去掉i这一维度,那么就变成了dp[j-1]+d[j]
。但这样做有一个问题,就是如果只保存一轮的数据,本轮的更新会将上一轮的结果覆盖掉,从前往后遍历的话那么后面的结果可能出问题。比如s=“bbb”,t=“bbb”
- s[0] = “b”, “b” =1
- s[1]=“b”, “b” = 2, “bb” = 1
这是正常情况的结果,但如果只保存一轮数据且从前往后遍历,那么第二步更新的时候会先将"b"更新为2,“bb"再更新的时候是"b”+“bb”,同样也变成了2,这样是不对的,原因就是更新覆盖了原来的数据。
解决的方法就是t从后往前遍历,因为更新值是dp[j-1]+dp[j],因此后面的数据被覆盖了也不会影响到前面的计算。这样就可以把dp数组从二维降低到一维,空间复杂度是 O ( m ) O(m) O(m),其中m为字符串t的长度。
代码
func numDistinct(s string, t string) int {
dp := make([]int, len(t) + 1)
dp[0] = 1
for i := 0; i < len(s); i++ {
for j := len(t) - 1; j >= 0; j-- {
if s[i] == t[j] {
dp[j+1] += dp[j]
}
}
}
return dp[len(t)]
}