Leetcode 115. 不同的子序列 降低空间复杂度的解法

2 篇文章 0 订阅

题目

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

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

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

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

分析

 之前写过时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m ) O(m) O(m)的DP解法,现在回过头来看反而花了很长时间才看懂,因此这里记录一下。

 首先很明确能看出来这是一道动态规划可解的题目,问题就是状态转移方程怎么写。在写方程之前我们手动计算下如何进行求解。因为题目要求的是子序列而不是子串,因此从头到尾遍历s,保存当前轮次下s子序列中关于字符串t的前缀,遍历完之后等于t的那个前缀的个数即答案,具体如下所示:

 以示例2为例子,从头到尾遍历s,并保存相应的前缀。

  1. s[0] == “b”,当前s子序列中关于t的前缀为 “b” = 1
  2. s[1] == “a”,相应前缀个数为 “b” = 1, “ba” = 1
  3. s[2] == “b”,相应前缀个数为 “b” = 2 “ba” = 1
  4. s[3] == “g”,相应前缀个数为 “b” = 2 “ba” = 1, “bag” = 1
  5. s[4] == “b”,相应前缀个数为 “b” = 3 “ba” = 1, “bag” = 1
  6. s[5] == “a”,相应前缀个数为 “b” = 3 “ba” = 4, “bag” = 1
  7. s[6] == “g”,相应前缀个数为 “b” = 3 “ba” = 4, “bag” = 5

 最终"bag"的个数即为答案5。通过这个过程我们可以发现,如果s的字符x等于t中的字符y,那么以y为结尾的前缀个数为上一轮中以y为结尾的前缀数和以y前一个字符为结尾的前缀数相加。比如,在上述例子的第6步,s[5] == “a”,那么这一轮"ba"的个数相当于s上一轮中“ba”的个数加上“b”的个数。如果s的字符不等于任何t的字符,那么所有前缀的个数保持不变。

 根据得到的结论可以写出状态转移方程。用dp[i][j]表示s中前i个字符中存在的以t[j]为结尾的前缀个数,可知状态转移方程如下所示
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] , i f s [ i ] = = t [ j ] d p [ i − 1 ] [ j ] , i f s [ i ] ! = t [ j ] dp[i][j] = \left\{ \begin{aligned} &dp[i-1][j-1] + dp[i-1][j], &if {\quad} s[i] == t[j] \\ &dp[i-1][j], &if {\quad} s[i]!= t[j] \end{aligned} \right. dp[i][j]={dp[i1][j1]+dp[i1][j],dp[i1][j],ifs[i]==t[j]ifs[i]!=t[j]
 注意j=0时,t相当于空字符串,s任意子串都包含t,因此dp[i][0] = 1

代码(golang)

func numDistinct(s string, t string) int {
    dp := make([][]int, len(s) + 1)
    for i := range dp {
        dp[i] = make([]int, len(t) + 1)
        dp[i][0] = 1
    }

    for i := 1; i < len(dp); i++ {
        for j := 1; j < len(dp[0]); 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[len(s)][len(t)]
}

空间复杂度优化

 观察到上述遍历过程中,每一轮更新只用到上一轮的结果,即第i次遍历只用第i-1次遍历时的结果。因此可以只保存两轮的状态值,这是优化的一个简单思路,此处不做赘述。

 我的思路是dp[i][j]更新时是dp[i-1][j-1]+dp[i-1][j],如果去掉i这一维度,那么就变成了dp[j-1]+d[j]。但这样做有一个问题,就是如果只保存一轮的数据,本轮的更新会将上一轮的结果覆盖掉,从前往后遍历的话那么后面的结果可能出问题。比如s=“bbb”,t=“bbb”

  1. s[0] = “b”, “b” =1
  2. s[1]=“b”, “b” = 2, “bb” = 1

 这是正常情况的结果,但如果只保存一轮数据且从前往后遍历,那么第二步更新的时候会先将"b"更新为2,“bb"再更新的时候是"b”+“bb”,同样也变成了2,这样是不对的,原因就是更新覆盖了原来的数据。

 解决的方法就是t从后往前遍历,因为更新值是dp[j-1]+dp[j],因此后面的数据被覆盖了也不会影响到前面的计算。这样就可以把dp数组从二维降低到一维,空间复杂度是 O ( m ) O(m) O(m),其中m为字符串t的长度。

代码

func numDistinct(s string, t string) int {
    dp := make([]int, len(t) + 1)
    dp[0] = 1
    for i := 0; i < len(s); i++ {
        for j := len(t) - 1; j >= 0; j-- {
            if s[i] == t[j] {
                dp[j+1] += dp[j]
            }
        }
    }
    return dp[len(t)]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值