115. 不同的子序列
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。
示例 1:
输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。
rabbbit
rabbbit
rabbbit
示例 2:
输入:s = “babgbag”, t = “bag”
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 “bag” 的方案。
class Solution {
public int numDistinct(String s, String t) {
int[][] dp = new int[s.length() + 1][t.length() + 1];
for (int i = 0; i < s.length() + 1; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < t.length() + 1; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.length()][t.length()];
}
}
这段代码是用于解决「不同子序列」问题的Java实现,实质上是计算字符串 t
在字符串 s
中的不同子序列出现的次数。给定两个字符串 s
和 t
,目标是找出 t
在 s
中有多少种不同的子序列出现方式。
代码解析
-
初始化动态规划数组:
- 创建一个二维数组
dp
,其大小为(s.length() + 1) x (t.length() + 1)
。dp[i][j]
的值代表s
的前i
个字符和t
的前j
个字符组成的子序列的出现次数。额外的一列是为了方便边界条件的处理,其中dp[i][0]
的值默认为1,因为任何字符串都是空字符串的子序列,且只有一种方式。
- 创建一个二维数组
-
动态规划迭代:
- 从1到
s.length()
遍历s
的每个字符,从1到t.length()
遍历t
的每个字符。 - 对于
s
的第i
个字符和t
的第j
个字符:- 如果两个字符相等,那么
s
的前i
个字符和t
的前j
个字符组成的子序列的出现次数等于s
的前i-1
个字符和t
的前j-1
个字符组成的子序列的出现次数加上s
的前i-1
个字符和t
的前j
个字符组成的子序列的出现次数,即dp[i - 1][j - 1] + dp[i - 1][j]
。 - 如果两个字符不相等,那么出现次数等于
s
的前i-1
个字符和t
的前j
个字符组成的子序列的出现次数,即dp[i - 1][j]
。
- 如果两个字符相等,那么
- 从1到
-
返回结果:
- 最后返回
dp[s.length()][t.length()]
,即t
在s
中的不同子序列出现的次数。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度: O(m * n),其中 m 和 n 分别是字符串
s
和t
的长度。这是因为需要遍历两个字符串的所有可能的字符组合来计算子序列的出现次数。 - 空间复杂度: O(m * n),需要一个大小为
(m + 1) x (n + 1)
的动态规划数组dp
来存储中间结果。
总结
这段代码通过动态规划的方法,有效地解决了不同子序列计数问题。尽管时间复杂度和空间复杂度较高,但在许多实际应用场景中,这样的性能通常是可接受的,特别是当字符串长度适中时。如果需要进一步优化空间复杂度,可以考虑使用滚动数组技术,将空间复杂度降低到 O(min(m, n)),但这通常会增加代码的复杂度。在处理此类问题时,权衡时间和空间资源是一个常见的考量因素。
583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4
方法一:
// dp数组中存储word1和word2最长相同子序列的长度
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return len1 + len2 - dp[len1][len2] * 2;
}
}
这段代码是用于解决「编辑距离之最短编辑脚本」问题的Java实现,但其计算方法并不直接针对编辑距离,而是利用了最长公共子序列(LCS, Longest Common Subsequence)的概念。给定两个字符串 word1
和 word2
,目标是计算将 word1
转换为 word2
所需的最少编辑操作数,这里的编辑操作包括插入、删除和替换一个字符。
代码解析
-
初始化动态规划数组:
- 创建一个二维数组
dp
,其大小为(len1 + 1) x (len2 + 1)
。dp[i][j]
的值代表word1
的前i
个字符和word2
的前j
个字符的最长公共子序列的长度。额外的一列和一行是为了方便边界条件的处理。
- 创建一个二维数组
-
动态规划迭代:
- 从1到
len1
遍历word1
的每个字符,从1到len2
遍历word2
的每个字符。 - 对于
word1
的第i
个字符和word2
的第j
个字符:- 如果两个字符相等,那么
word1
的前i
个字符和word2
的前j
个字符的最长公共子序列的长度等于word1
的前i-1
个字符和word2
的前j-1
个字符的最长公共子序列的长度加1,即dp[i-1][j-1] + 1
。 - 如果两个字符不相等,那么最长公共子序列的长度等于
word1
的前i-1
个字符和word2
的前j
个字符的最长公共子序列的长度与word1
的前i
个字符和word2
的前j-1
个字符的最长公共子序列的长度中的较大值,即Math.max(dp[i-1][j], dp[i][j-1])
。
- 如果两个字符相等,那么
- 从1到
-
计算编辑距离:
- 最长公共子序列的长度可以被视为两个字符串共享的字符数量。为了将
word1
转换成word2
,我们需要删除word1
中不在公共子序列中的字符,并在适当位置插入word2
中不在公共子序列中的字符。因此,编辑距离等于word1
和word2
的长度之和减去两倍的最长公共子序列的长度,即len1 + len2 - dp[len1][len2] * 2
。
- 最长公共子序列的长度可以被视为两个字符串共享的字符数量。为了将
时间复杂度和空间复杂度
- 时间复杂度: O(m * n),其中 m 和 n 分别是字符串
word1
和word2
的长度。这是因为需要遍历两个字符串的所有可能的字符组合来计算最长公共子序列的长度。 - 空间复杂度: O(m * n),需要一个大小为
(m + 1) x (n + 1)
的动态规划数组dp
来存储中间结果。
总结
这段代码通过计算最长公共子序列并利用其长度来间接计算编辑距离,提供了一种有效的解决方案。尽管它并没有直接使用编辑距离的动态规划公式,但通过巧妙地利用最长公共子序列的概念,仍然达到了计算编辑距离的目的。这种间接的方法有时在处理某些变体问题时会更加灵活和直观。如果需要进一步优化空间复杂度,可以考虑使用滚动数组技术,将空间复杂度降低到 O(min(m, n)),但这可能会使代码变得更复杂。
方法二:
// dp数组中存储需要删除的字符个数
class Solution {
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length() + 1][word2.length() + 1];
for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;
for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;
for (int i = 1; i < word1.length() + 1; i++) {
for (int j = 1; j < word2.length() + 1; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
}
}
}
return dp[word1.length()][word2.length()];
}
}
这段代码是用于解决「编辑距离」问题的Java实现,该问题要求计算将字符串 word1
转换为字符串 word2
所需的最小编辑操作数。编辑操作包括插入、删除和替换一个字符。代码通过动态规划方法实现,直接计算最小编辑距离。
代码解析
-
初始化动态规划数组:
- 创建一个二维数组
dp
,其大小为(word1.length() + 1) x (word2.length() + 1)
。dp[i][j]
的值代表将word1
的前i
个字符转换为word2
的前j
个字符所需的最小编辑操作数。额外的一列和一行是为了方便边界条件的处理,其中dp[i][0]
的值初始化为i
,dp[0][j]
的值初始化为j
,因为将一个字符串转换为空字符串或从空字符串转换为一个字符串所需的操作数分别为字符串的长度。
- 创建一个二维数组
-
动态规划迭代:
- 从1到
word1.length() + 1
遍历word1
的每个字符,从1到word2.length() + 1
遍历word2
的每个字符。 - 对于
word1
的第i
个字符和word2
的第j
个字符:- 如果两个字符相等,那么将
word1
的前i
个字符转换为word2
的前j
个字符所需的最小编辑操作数等于将word1
的前i-1
个字符转换为word2
的前j-1
个字符所需的最小编辑操作数,即dp[i - 1][j - 1]
。 - 如果两个字符不相等,那么最小编辑操作数等于以下三种操作中的最小值:
- 将
word1
的第i
个字符替换为word2
的第j
个字符,加上将word1
的前i-1
个字符转换为word2
的前j-1
个字符所需的最小编辑操作数加2(因为替换操作成本定义为2)。 - 删除
word1
的第i
个字符,加上将word1
的前i-1
个字符转换为word2
的前j
个字符所需的最小编辑操作数加1。 - 在
word1
的前i
个字符后插入word2
的第j
个字符,加上将word1
的前i
个字符转换为word2
的前j-1
个字符所需的最小编辑操作数加1。
- 将
- 如果两个字符相等,那么将
- 从1到
-
返回结果:
- 最后返回
dp[word1.length()][word2.length()]
,即word1
转换为word2
所需的最小编辑操作数。
- 最后返回
时间复杂度和空间复杂度
- 时间复杂度: O(m * n),其中 m 和 n 分别是字符串
word1
和word2
的长度。这是因为需要遍历两个字符串的所有可能的字符组合来计算最小编辑操作数。 - 空间复杂度: O(m * n),需要一个大小为
(m + 1) x (n + 1)
的动态规划数组dp
来存储中间结果。
总结
这段代码通过动态规划方法,有效地解决了编辑距离问题,直接计算出将一个字符串转换为另一个字符串所需的最小编辑操作数。如果需要进一步优化空间复杂度,可以考虑使用滚动数组技术,将空间复杂度降低到 O(min(m, n)),但这可能会使代码变得更加复杂。在实际应用中,选择合适的优化策略取决于具体的需求和资源限制。
方法三:
//DP - longest common subsequence (用最長公共子序列反推)
class Solution {
public int minDistance(String word1, String word2) {
char[] char1 = word1.toCharArray();
char[] char2 = word2.toCharArray();
int len1 = char1.length;
int len2 = char2.length;
int dp[][] = new int [len1 + 1][len2 + 1];
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; j++){
if(char1[i - 1] == char2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return len1 + len2 - (2 * dp[len1][len2]);//和leetcode 1143只差在這一行。
}
}
这段代码是用于解决「编辑距离」问题的Java实现,但它实际上是通过计算两个字符串 word1
和 word2
的最长公共子序列(LCS, Longest Common Subsequence)的长度,然后基于这个信息来间接计算编辑距离。这种方法的思路是,编辑距离可以看作是两个字符串各自去除最长公共子序列部分后,剩下的字符数总和。
代码解析
-
初始化动态规划数组:
- 创建一个二维数组
dp
,其大小为(len1 + 1) x (len2 + 1)
。dp[i][j]
的值代表word1
的前i
个字符和word2
的前j
个字符的最长公共子序列的长度。额外的一列和一行是为了方便边界条件的处理。
- 创建一个二维数组
-
动态规划迭代:
- 从1到
len1
遍历word1
的每个字符,从1到len2
遍历word2
的每个字符。 - 对于
word1
的第i
个字符和word2
的第j
个字符:- 如果两个字符相等,那么
word1
的前i
个字符和word2
的前j
个字符的最长公共子序列的长度等于word1
的前i-1
个字符和word2
的前j-1
个字符的最长公共子序列的长度加1,即dp[i-1][j-1] + 1
。 - 如果两个字符不相等,那么最长公共子序列的长度等于
word1
的前i-1
个字符和word2
的前j
个字符的最长公共子序列的长度与word1
的前i
个字符和word2
的前j-1
个字符的最长公共子序列的长度中的较大值,即Math.max(dp[i-1][j], dp[i][j-1])
。
- 如果两个字符相等,那么
- 从1到
-
计算编辑距离:
- 最长公共子序列的长度可以被视为两个字符串共享的字符数量。为了将
word1
转换成word2
,我们需要删除word1
中不在公共子序列中的字符,并在适当位置插入word2
中不在公共子序列中的字符。因此,编辑距离等于word1
和word2
的长度之和减去两倍的最长公共子序列的长度,即len1 + len2 - (2 * dp[len1][len2])
。
- 最长公共子序列的长度可以被视为两个字符串共享的字符数量。为了将
时间复杂度和空间复杂度
- 时间复杂度: O(m * n),其中 m 和 n 分别是字符串
word1
和word2
的长度。这是因为需要遍历两个字符串的所有可能的字符组合来计算最长公共子序列的长度。 - 空间复杂度: O(m * n),需要一个大小为
(m + 1) x (n + 1)
的动态规划数组dp
来存储中间结果。
总结
这段代码通过计算最长公共子序列并利用其长度来间接计算编辑距离,提供了一种有效的解决方案。尽管这种方法并不是直接计算编辑距离的标准动态规划公式,但通过计算最长公共子序列的长度,然后根据这个信息推导出编辑距离,同样能高效地解决问题。这种方法在某些情况下可能更为直观,尤其是在理解最长公共子序列概念的基础上。如果需要进一步优化空间复杂度,可以考虑使用滚动数组技术,将空间复杂度降低到 O(min(m, n)),但这可能会使代码变得更复杂。