LeetCode 第115题:不同的子序列

87 阅读7分钟

LeetCode 第115题:不同的子序列

题目描述

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE""ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

难度

困难

题目链接

点击在LeetCode中查看题目

示例

示例 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
    ^^^

提示

  • 1 <= s.length, t.length <= 1000
  • st 由英文字母组成

解题思路

方法一:动态规划

这道题可以使用动态规划来解决。我们定义 dp[i][j] 表示字符串 s 的前 i 个字符中,子序列 t 的前 j 个字符出现的次数。

关键点:

  • 状态定义:dp[i][j] 表示 s[0...i-1] 中子序列 t[0...j-1] 出现的次数
  • 边界条件:dp[i][0] = 1,表示空字符串是任何字符串的子序列,且只有1种方案
  • 状态转移:
    • 如果 s[i-1] == t[j-1],则 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
    • 如果 s[i-1] != t[j-1],则 dp[i][j] = dp[i-1][j]

具体步骤:

  1. 初始化 dp 数组,大小为 (m+1) × (n+1),其中 ms 的长度,nt 的长度
  2. 初始化边界条件:dp[i][0] = 1,对于所有 0 <= i <= m
  3. 遍历 st,填充 dp 数组:
    • 如果 s[i-1] == t[j-1],则 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
    • 如果 s[i-1] != t[j-1],则 dp[i][j] = dp[i-1][j]
  4. 返回 dp[m][n]

时间复杂度:O(m×n),其中 m 是 s 的长度,n 是 t 的长度 空间复杂度:O(m×n),需要一个二维数组存储状态

方法二:动态规划(空间优化)

我们可以观察到,dp[i][j] 只与 dp[i-1][j-1]dp[i-1][j] 有关,因此可以使用滚动数组优化空间复杂度。

关键点:

  • 使用一维数组 dp[j] 表示当前 s 的前 i 个字符中,子序列 t 的前 j 个字符出现的次数
  • 从右向左更新 dp 数组,避免覆盖还未使用的值

具体步骤:

  1. 初始化一维 dp 数组,大小为 n+1,其中 nt 的长度
  2. 初始化边界条件:dp[0] = 1
  3. 遍历 st,更新 dp 数组:
    • 从右向左遍历 j,从 n1
    • 如果 s[i-1] == t[j-1],则 dp[j] = dp[j-1] + dp[j]
    • 如果 s[i-1] != t[j-1],则 dp[j] 保持不变
  4. 返回 dp[n]

时间复杂度:O(m×n),其中 m 是 s 的长度,n 是 t 的长度 空间复杂度:O(n),只需要一个一维数组存储状态

图解思路

方法一:动态规划过程分析表

以示例1为例,s = "rabbbit", t = "rabbit"

dp[i][j]j=0 ("")j=1 ("r")j=2 ("ra")j=3 ("rab")j=4 ("rabb")j=5 ("rabbi")j=6 ("rabbit")
i=0 ("")1000000
i=1 ("r")1100000
i=2 ("ra")1110000
i=3 ("rab")1111000
i=4 ("rabb")1111100
i=5 ("rabbb")1111200
i=6 ("rabbbi")1111230
i=7 ("rabbbit")1111233

方法二:空间优化过程表

以示例1为例,s = "rabbbit", t = "rabbit"

迭代dp[0]dp[1]dp[2]dp[3]dp[4]dp[5]dp[6]当前字符
初始化1000000-
i=1 (s[0]='r')1100000r
i=2 (s[1]='a')1110000a
i=3 (s[2]='b')1111000b
i=4 (s[3]='b')1111100b
i=5 (s[4]='b')1111200b
i=6 (s[5]='i')1111230i
i=7 (s[6]='t')1111233t

代码实现

C# 实现

public class Solution {
    // 方法一:动态规划
    public int NumDistinct(string s, string t) {
        int m = s.Length;
        int n = t.Length;
        
        // 创建dp数组
        long[,] dp = new long[m + 1, n + 1];
        
        // 初始化边界条件
        for (int i = 0; i <= m; i++) {
            dp[i, 0] = 1;
        }
        
        // 填充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];
                }
            }
        }
        
        return (int)dp[m, n];
    }
    
    // 方法二:动态规划(空间优化)
    public int NumDistinctOptimized(string s, string t) {
        int m = s.Length;
        int n = t.Length;
        
        // 创建一维dp数组
        long[] dp = new long[n + 1];
        
        // 初始化边界条件
        dp[0] = 1;
        
        // 填充dp数组
        for (int i = 1; i <= m; i++) {
            for (int j = n; j >= 1; j--) {
                if (s[i - 1] == t[j - 1]) {
                    dp[j] = dp[j - 1] + dp[j];
                }
                // 如果不相等,dp[j]保持不变
            }
        }
        
        return (int)dp[n];
    }
}

Python 实现

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
        
        # 填充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]
    
    # 方法二:动态规划(空间优化)
    def numDistinctOptimized(self, s: str, t: str) -> int:
        m, n = len(s), len(t)
        
        # 创建一维dp数组
        dp = [0] * (n + 1)
        
        # 初始化边界条件
        dp[0] = 1
        
        # 填充dp数组
        for i in range(1, m + 1):
            for j in range(n, 0, -1):
                if s[i - 1] == t[j - 1]:
                    dp[j] = dp[j - 1] + dp[j]
                # 如果不相等,dp[j]保持不变
        
        return dp[n]

C++ 实现

class Solution {
public:
    // 方法一:动态规划
    int numDistinct(string s, string t) {
        int m = s.length();
        int n = t.length();
        
        // 创建dp数组,使用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;
        }
        
        // 填充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];
                }
            }
        }
        
        return dp[m][n];
    }
    
    // 方法二:动态规划(空间优化)
    int numDistinctOptimized(string s, string t) {
        int m = s.length();
        int n = t.length();
        
        // 创建一维dp数组
        vector<unsigned long long> dp(n + 1, 0);
        
        // 初始化边界条件
        dp[0] = 1;
        
        // 填充dp数组
        for (int i = 1; i <= m; i++) {
            for (int j = n; j >= 1; j--) {
                if (s[i - 1] == t[j - 1]) {
                    dp[j] = dp[j - 1] + dp[j];
                }
                // 如果不相等,dp[j]保持不变
            }
        }
        
        return dp[n];
    }
};

执行结果

C# 实现

  • 执行用时:76 ms
  • 内存消耗:38.9 MB

Python 实现

  • 执行用时:44 ms
  • 内存消耗:17.2 MB

C++ 实现

  • 执行用时:0 ms
  • 内存消耗:6.5 MB

性能对比

语言执行用时内存消耗特点
C#76 ms38.9 MB代码结构清晰,但性能较慢
Python44 ms17.2 MB代码简洁,性能适中
C++0 ms6.5 MB执行速度最快,内存占用最少

代码亮点

  1. 🎯 使用动态规划解决子序列计数问题,思路清晰
  2. 💡 空间优化方法将空间复杂度从O(m×n)降低到O(n)
  3. 🔍 正确处理了数据范围可能导致的溢出问题,使用更大的整数类型
  4. 🎨 代码结构清晰,变量命名规范,易于理解和维护

常见错误分析

  1. 🚫 没有考虑到结果可能超出32位整数范围,导致溢出
  2. 🚫 空间优化时从左到右更新dp数组,导致使用了已经被更新的值
  3. 🚫 初始化边界条件错误,例如忘记设置dp[0]=1
  4. 🚫 状态转移方程理解错误,导致计算结果不正确

解法对比

解法时间复杂度空间复杂度优点缺点
动态规划O(m×n)O(m×n)思路直观,易于理解空间消耗较大
动态规划(空间优化)O(m×n)O(n)空间效率高代码稍复杂,需要注意更新顺序

相关题目