文章目录
- 一、题目描述
- 二、解题思路
- 1. 问题分析
- 2. 动态规划的定义
- 3. 状态转移方程
- 4. 边界条件
- 三、代码实现
- 四、代码详解
- 五、总结
一、题目描述
「力扣 115. 不同的子序列」是一道经典的动态规划题,题目的描述如下:
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。子序列表示通过删除 s
中的某些字符(可以是 0 个或者多个)得到的新字符串,且不改变字符的相对顺序。
示例:
输入:s = "rabbbit", t = "rabbit"
输出:3
二、解题思路
1. 问题分析
题目要求我们找出字符串 t
在字符串 s
的不同子序列中的出现次数。我们可以考虑用动态规划来解决这个问题。
首先,我们需要明确的是:
- 字符串
t
出现在字符串s
的子序列中,意味着t
的字符顺序必须和s
中的某些字符相同,且这些字符不需要连续,但必须保持顺序一致。 - 我们需要求出有多少种方法可以通过删除
s
的某些字符使得剩下的字符恰好组成t
。
2. 动态规划的定义
为了进行状态转移,我们定义一个二维数组 dp[i][j]
:
dp[i][j]
表示使用s[0:i]
的前i
个字符,能够形成t[0:j]
的前j
个字符的子序列个数。
特别地:
- 当
j = 0
时,t
是空字符串,空字符串是所有字符串的子序列,因此对于任何i
,dp[i][0] = 1
。 - 当
i = 0
且j > 0
时,dp[0][j] = 0
,因为空的s
无法生成非空的t
。
3. 状态转移方程
我们可以通过遍历字符串 s
和 t
来填充 dp
表。具体的状态转移分为两种情况:
- 如果
s[i-1] != t[j-1]
,说明我们不能使用s[i-1]
这个字符来匹配t[j-1]
,因此dp[i][j] = dp[i-1][j]
。 - 如果
s[i-1] == t[j-1]
,我们有两个选择:- 不使用
s[i-1]
来匹配t[j-1]
,此时dp[i][j] = dp[i-1][j]
。 - 使用
s[i-1]
来匹配t[j-1]
,此时dp[i][j] = dp[i-1][j-1]
。
- 不使用
因此,当 s[i-1] == t[j-1]
时,状态转移方程为:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
4. 边界条件
dp[0][0] = 1
表示空的s
和空的t
是可以匹配的。dp[i][0] = 1
表示t
为空时总是可以匹配任意的s
。
三、代码实现
下面是使用Go语言实现的具体代码:
package mainimport "fmt"// 不同的子序列
func numDistinct(s string, t string) int {m, n := len(s), len(t)// 初始化一个 (m+1) x (n+1) 的二维数组dp := make([][]int, m+1)for i := range dp {dp[i] = make([]int, n+1)}// 边界条件:dp[i][0] = 1,表示空的 t 总是 s 的子序列for i := 0; i <= m; i++ {dp[i][0] = 1}// 填充 dp 数组for i := 1; i <= m; i++ {for j := 1; j <= n; 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]}}}// 返回结果return dp[m][n]
}func main() {s := "rabbbit"t := "rabbit"fmt.Println(numDistinct(s, t)) // 输出 3
}
四、代码详解
-
初始化二维数组
dp
:
我们创建一个二维数组dp
,大小为(m+1) x (n+1)
,其中m
是字符串s
的长度,n
是字符串t
的长度。之所以要多加一行和一列,是为了处理边界情况,确保我们可以正确地填充dp
表。 -
设置边界条件:
dp[i][0] = 1
,表示空的t
始终是s
的子序列。dp[0][j] = 0
,当s
为空时,无法匹配非空的t
。
-
填充
dp
表:- 当
s[i-1] == t[j-1]
时,表示我们有两种选择,既可以不使用s[i-1]
来匹配,也可以使用。因此我们将两种情况的结果相加。 - 当
s[i-1] != t[j-1]
时,说明s[i-1]
无法参与匹配,这时结果只能是dp[i-1][j]
。
- 当
-
返回结果:
最终,dp[m][n]
就是我们所求的t
在s
中作为子序列出现的次数。
五、总结
本题的核心在于通过动态规划解决字符串的子序列问题。通过构造一个二维数组来记录每个子问题的解,并且通过合理的状态转移公式来进行递推,最终得到结果。这种解法的时间复杂度为 O(m * n)
,空间复杂度也是 O(m * n)
,适用于中等规模的输入。
通过这道题的练习,我们不仅巩固了动态规划的基础知识,还学会了如何在 Go 语言中高效地实现动态规划算法。
希望这篇文章对你有所帮助!如果你有其他问题或者想法,欢迎在评论区讨论!