LeetCode 940. 不同的子序列 II 题解与拓展:代码实现及知识记录
2023-12-28 07:23:58
动态规划解题:LeetCode 940. 不同的子序列 II
在解决算法问题时,动态规划是一种强大的技术,它可以帮助我们通过将问题分解成更小的子问题并利用已解决的子问题的解来有效地解决复杂问题。在本文中,我们将探索如何使用动态规划来解决 LeetCode 上一道经典问题——940. 不同的子序列 II。
问题
LeetCode 940. 不同的子序列 II 要求我们计算一个字符串中不同子序列的数量,其中子序列是不允许包含重复字符的连续字符序列。例如,对于字符串 "abc",其不同的子序列包括 "a"、"b"、"c"、"ab"、"ac" 和 "bc",共 6 个。
动态规划算法
动态规划是一种自底向上的算法,它将问题分解成一系列重叠的子问题,然后通过解决较小的子问题来逐渐解决整个问题。对于 940. 不同的子序列 II,我们将使用一个二维数组 dp
来存储子问题的结果。其中,dp[i][j]
表示考虑前 i
个字符,以第 j
个字符结尾的不同子序列数量。
状态转移方程
对于每个字符 s[i]
,我们有两种选择:
- 将
s[i]
添加到子序列中。此时,dp[i][j]
等于dp[i-1][j]
,因为不包含s[i]
时以第j
个字符结尾的不同子序列数量与包含s[i]
时相同。 - 不将
s[i]
添加到子序列中。此时,dp[i][j]
等于dp[i-1][j-1]
,因为不包含s[i]
时以第j-1
个字符结尾的不同子序列数量与包含s[i]
时以第j
个字符结尾的不同子序列数量相同。
初始化
我们将 dp[0][j]
初始化为 0,表示没有任何字符时,任何字符结尾的子序列数量都为 0。
时间复杂度
该算法的时间复杂度为 O(n^2),其中 n 为字符串的长度。这是因为该算法需要遍历字符串中的每个字符,并对每个字符进行两次子序列计数。
代码示例
下面是该算法的 Java 代码实现:
class Solution {
public int distinctSubseqII(String s) {
int mod = 1000000007;
int n = s.length();
int[] last = new int[26];
Arrays.fill(last, -1);
long[] dp = new long[n + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) {
dp[i] = dp[i - 1] * 2 % mod;
int index = s.charAt(i - 1) - 'a';
if (last[index] != -1) {
dp[i] -= dp[last[index]];
dp[i] = (dp[i] + mod) % mod;
}
last[index] = i - 1;
}
return (int) (dp[n] - 1);
}
}
应用场景
动态规划在解决各种算法问题中有着广泛的应用,包括:
- 最长公共子序列: 寻找两个字符串的最长公共子序列,即两个字符串中长度最长的相同子序列。
- 背包问题: 在给定的容量和价值限制下,确定装入背包中的物品组合以最大化总价值。
- 编辑距离: 计算将一个字符串转换为另一个字符串所需的最小编辑操作次数。
总结
通过使用动态规划,我们可以有效地解决 LeetCode 940. 不同的子序列 II 问题。该算法基于子问题的重叠性,将问题分解成更小的子问题,并逐步解决。该算法的时间复杂度为 O(n^2),其中 n 为字符串的长度。动态规划是一种强大的技术,它在解决各种算法问题中有着广泛的应用。
常见问题解答
Q1:动态规划算法为什么比朴素递归算法更有效率?
A1:动态规划算法利用了子问题的重叠性,避免了重复计算。它通过存储子问题的解,从而显著减少了时间复杂度。
Q2:状态转移方程是如何推导出来的?
A2:状态转移方程是基于这样一个事实:一个子序列要么包含当前字符,要么不包含。因此,我们可以使用两种情况来计算子序列的数量。
Q3:dp[0][j]
初始化为 0 的目的是什么?
A3:dp[0][j]
初始化为 0 表示没有任何字符时,以任何字符结尾的子序列数量都为 0。这为算法提供了一个清晰的起点。
Q4:该算法是否适用于包含重复字符的字符串?
A4:该算法不适用于包含重复字符的字符串。为了解决这个问题,需要使用更高级的算法,例如基于后缀数组的算法。
Q5:动态规划算法的局限性是什么?
A5:动态规划算法的局限性在于它可能需要大量的空间和时间来存储和计算子问题的解。因此,它不适用于非常大的问题。