在问 · 算法题解

115. 不同的子序列

困难动态规划二维动态规划力扣原题 ↗

题目描述

输入两个字符串 s 和 t。从 s 中删掉若干字符(也可以不删),剩下的字符保持原有次序,得到的就是 s 的一个子序列。

统计有多少种选取方式,使得到的子序列恰好等于 t。只要选中的下标集合不同,就算作不同的方式。

返回方式总数。数据保证答案不超过 32 位有符号整数的范围。

示例 1
输入:s = "rabbbit", t = "rabbit"
输出:3

解释:s 中有三个连续的 b,从中任选两个,共 3 种方式拼出 rabbit。

示例 2
输入:s = "babgbag", t = "bag"
输出:5

解释:按选中位置不同,共有 5 种方式从 s 中取出 bag。

数据范围

  • 1 <= s.length, t.length <= 1000
  • s 和 t 只含英文字母。

解题思路:二维动态规划

设 dp[i][j] 为 s 前 i 个字符中等于 t 前 j 个字符的子序列个数。dp[i][0] = 1(空串只有一种选法)。对 s[i-1]:不用它,有 dp[i-1][j] 种;如果 s[i-1] == t[j-1],还可以用它匹配 t[j-1],再加 dp[i-1][j-1] 种。

为什么正确

每行只依赖上一行的正上方和左上方。

复杂度分析

时间 O(mn),空间 O(mn)(可压缩为 O(n))。

易错点

即使 s[i-1] == t[j-1],也要保留“不用它”的 dp[i-1][j],否则会漏掉用后面相同字符的选法。中间值可能很大,C++ / Java 要用更宽的类型。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        m, n = len(s), len(t)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(m + 1):
            dp[i][0] = 1
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                dp[i][j] = dp[i - 1][j]
                if s[i - 1] == t[j - 1]:
                    dp[i][j] += dp[i - 1][j - 1]
        return dp[m][n]

载入编辑器,自己跑一遍 →

其他解法

两种解法的状态转移完全相同;二维表更直观,一维滚动数组把空间压到 O(n)。

解法 2:一维滚动数组 时间 O(m·n),空间 O(n)

dp[j] 表示当前已扫描的 s 前缀中,子序列等于 t 前 j 个字符的方案数,dp[0] = 1。每读入 s 的一个字符 ch,让 j 从 n 倒序到 1;若 ch == t[j-1],则 dp[j] += dp[j-1]。倒序保证 dp[j-1] 仍是上一行的值。

二维状态表压缩成一行,空间从 O(m·n) 降到 O(n)。代价是丢失中间状态,必须理解为什么要倒序更新。

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        n = len(t)
        dp = [0] * (n + 1)
        dp[0] = 1
        for ch in s:
            for j in range(n, 0, -1):
                if ch == t[j - 1]:
                    dp[j] += dp[j - 1]
        return dp[n]

动态规划 专题的其他题目

查看 动态规划 全部题目 →