动态规划—115. 不同的子序列
- 前言
- 题目描述
- 基本思路
- 1. 问题定义
- 2. 理解问题和递推关系
- 3. 解决方法
- 3.1 动态规划方法
- 3.2 空间优化的动态规划
- 4. 进一步优化
- 5. 小总结
- 代码实现
- Python
- Python3代码实现
- Python 代码解释
- C++
- C++代码实现
- C++ 代码解释
- 1. 变量定义:
- 2. 初始化:
- 3. 动态规划状态转移:
- 4. 返回结果:
- 总结:
前言
在字符串处理的领域,不同子序列问题是一个经典的挑战,涉及到如何计算一个字符串的所有不同子序列以匹配另一个字符串。通过动态规划方法,我们能够有效地找出字符串之间的匹配数量,为更复杂的字符串问题提供解决方案。本文将详细介绍这一问题的思路、解决方法以及相应的 Python 和 C++ 实现代码。
题目描述
基本思路
1. 问题定义
不同子序列问题要求我们计算字符串 s s s 中有多少个不同的子序列等于字符串 t t t 。子序列是通过删除字符串中的某些字符(可以不删除任何字符)而形成的序列。
2. 理解问题和递推关系
- 我们可以定义一个二维数组 d p [ i ] [ j ] d p[i][j] dp[i][j] ,表示字符串 s s s 的前 i i i 个字符与字符串 t t t 的前 j j j 个字符形成的不同子序列的数量。
- 递推关系如下:
- 如果 s [ i − 1 ] = = t [ j − 1 ] s[i-1]==t[j-1] s[i−1]==t[j−1] ,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j-1]+d p[i-1][j] dp[i][j]=dp[i−1][j−1]+dp[i−1][j] 。这表示我们可以选择将 s [ i − 1 ] s[i-1] s[i−1] 作为 t [ j − 1 ] t[j-1] t[j−1] 的一部分,或者不选择它。
- 如果 s [ i − 1 ] ! = t [ j − 1 ] s[i-1]!=t[j-1] s[i−1]!=t[j−1], 则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j] dp[i][j]=dp[i−1][j] 。这表示我们只能忽略 s [ i − 1 ] s[i-1] s[i−1] 来继续匹配 t [ j − 1 ] t[j-1] t[j−1] 。
3. 解决方法
3.1 动态规划方法
- 创建一个二维数组 d p d p dp ,大小为 ( m + 1 ) × ( n + 1 ) (m+1) \times(n+1) (m+1)×(n+1) ,其中 m m m 是字符串 s s s 的长度, n n n 是字符串 t t t 的长度。
- 初始化边界条件:
- d p [ 0 ] [ 0 ] = 1 \mathrm{dp}[0][0]=1 dp[0][0]=1 :两个空字符串是一个有效的子序列。
- d p [ i ] [ θ ] = 1 d p[i][\theta]=1 dp[i][θ]=1 :任何字符串 s s s 的前 i i i 个字符与空字符串匹配的方式只有一种,即删除所有字符。
- d p [ 0 ] [ j ] = 0 d p[0][j]=0 dp[0][j]=0 :空字符串无法匹配非空字符串。
- 使用双重矿环填充 dp 数组,依赖于前面的状态。
- 最终结果为 d p [ m ] [ n ] d p[m][n] dp[m][n] 。
3.2 空间优化的动态规划
- 可以使用一维数组来优化空间复杂度,将 dp 数组从二维变为一维。
4. 进一步优化
- 使用一维数组可以减少内存使用,同时时间复杂度保持在 O ( m ∗ n ) O(m * n) O(m∗n) ,适合处理中等规模的字符串。
5. 小总结
- 不同子序列问题通过动态规划有效计算出字符串之间的匹配数量。
- 理解该问题的解决方案有助于掌握动态规划的设计理念,并能够应用于类似的字符串匹配问题。
以上就是不同的子序列问题的基本思路。
代码实现
Python
Python3代码实现
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
# 创建dp数组
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界条件
for i in range(m + 1):
dp[i][0] = 1 # 空字符串t的匹配方法
# 填充dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
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[m][n]
Python 代码解释
- 初始化:创建
dp
数组并设置边界条件,处理空字符串的匹配。 - 动态规划填充:通过双重循环遍历
s
和t
的每个字符,依据字符相同与否来更新dp
数组。 - 返回结果:最终返回
dp[m][n]
,即字符串s
中与t
匹配的不同子序列数量。
C++
C++代码实现
class Solution {
public:
int numDistinct(string s, string t) {
int m = s.size(), n = t.size();
// 使用unsigned long long类型来防止溢出
vector<vector<unsigned long long>> dp(m + 1, vector<unsigned long long>(n + 1, 0));
// 初始化边界条件
for (int i = 0; i <= m; i++) {
dp[i][0] = 1; // 空字符串t的匹配方法
}
// 填充dp数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; 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]; // 字符不同
}
}
}
// 返回不同子序列的数量,确保返回的结果是int类型
return static_cast<int>(dp[m][n]);
}
};
C++ 代码解释
- 初始化:创建
dp
数组并设置边界条件,处理空字符串的匹配情况。 - 动态规划填充:通过双重循环更新
dp
数组,依赖于当前字符的匹配状态。 - 返回结果:返回
dp[m][n]
,即不同子序列的数量。
1. 变量定义:
- m : s m: s m:s 字符串的长度。
- n : t n: t n:t 字符串的长度。
- d p : d p: dp:一个二维动态规划数组,用于存储从 s s s 的前 i i i 个字符中选择与 t t t 的前 j j j 个字符匹配的子序列个数。 d p [ i ] [ j ] d p[i][j] dp[i][j] 的值表示从 s s s 的前 i i i 个字符中有多少个不同子序列可以匹配 t t t 的前 j j j 个字符。
2. 初始化:
- d p [ i ] [ θ ] = 1 d p[i][\theta]=1 dp[i][θ]=1 :当 t t t 是空字符串时,不管 s s s 是什么,只有一种方法可以匹配空字符串,即删除所有字符。因此, d p [ i ] [ 0 ] = 1 d p[i][0]=1 dp[i][0]=1 。
- d p [ 0 ] [ j ] = 0 d p[0][j]=0 dp[0][j]=0 :当 s s s 是空字符串而 t t t 不是空字符串时,不可能通过任何方式匹配,因此 d p [ 0 ] [ j ] = 0 \mathrm{dp}[0][j]=0 dp[0][j]=0 。
3. 动态规划状态转移:
- 字符相同: 如果 s [ i − 1 ] = = t [ j − 1 ] s[i-1]==\mathrm{t}[j-1] s[i−1]==t[j−1] ,有两种选择:
- 不使用 s [ i − 1 ] s[i-1] s[i−1] ,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j] dp[i][j]=dp[i−1][j] ,忽略 s [ i − 1 ] s[i-1] s[i−1] ,继续用前面的字符来匹配 t [ j ] t[j] t[j] 。
- 使用 s [ i − 1 ] s[i-1] s[i−1], 即 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] d p[i][j]=d p[i-1][j-1] dp[i][j]=dp[i−1][j−1] ,让 s [ i − 1 ] s[i-1] s[i−1] 和 t [ j − 1 ] t[j-1] t[j−1] 匹配。
- 最终 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j-1]+d p[i-1][j] dp[i][j]=dp[i−1][j−1]+dp[i−1][j] ,两者相加表示两种选择的总数。
- 字符不同: 如果 s [ i − 1 ] ! = t [ j − 1 ] s[i-1]!=t[j-1] s[i−1]!=t[j−1] ,只能忽略 s [ i − 1 ] s[i-1] s[i−1] ,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j] dp[i][j]=dp[i−1][j] ,表示继续用 s s s 的前 i − 1 i-1 i−1 个字符来匹配 t [ j ] t[j] t[j] 。
4. 返回结果:
- 最终结果保存在 d p [ m ] [ n ] d p[m][n] dp[m][n] 中,它表示字符串 s s s 中有多少个不同的子序列与 t t t 完全匹配。
- 由于动态规划数组使用的是
unsigned long long
来避免溢出,但题目要求返回 int 类型的结果,因此最后使用static_cast<int>
将其转换为int
。
总结:
- 不同子序列问题是动态规划的一个重要应用,展示了如何通过状态转移方程来计算字符串之间的匹配。
- 理解该问题的解决方案可以帮助掌握动态规划的核心思想,并为处理其他类似问题提供借鉴。
- 本文提供的代码实现既清晰又高效,为学习和应用动态规划提供了实际示例。