代码随想录算法训练营Day55 | 392. 判断子序列 | 115. 不同的子序列

文章目录

  • 392. 判断子序列
    • dp - 编辑距离入门
    • dp - 传递 bool 来确定编辑
    • 双指针
  • 115. 不同的子序列

392. 判断子序列

题目链接 | 解题思路

乍一看本题和之前的题没什么关系,只是一道简单的双指针。但实际上,这是一道特殊版本的最长公共子序列,同时也是编辑距离的入门。作为编辑距离的题,本题只考虑删除元素即可。

dp - 编辑距离入门

题目要求判断 s 是否是 t 的子序列,其实相当于要求 s 和 t 的最大公共子序列的长度就是 len(s)

  1. dp 数组的下标含义:dp[i][j]s[:i+1]t[:j+1] 的最大公共子序列的长度

  2. dp 递推公式:

    • 如果 s[i] == t[j],可以得到 dp[i][j] = dp[i-1][j-1] + 1

    • 否则,我们就考虑 t[:j] 从而得到 dp[i][j] = dp[i][j-1](代码随想录的递推公式)

      • 这里有一个比较令人费解的状态递推:为什么直接得到 dp[i][j] = dp[i][j-1],而不是像之前一样 dp[i][j] = max(dp[i][j-1], dp[i-1][j])
      • 这涉及到了利用最大公共子序列的长度来进行本题的抽象化的一个要求:这个抽象化的结果要和 len(s) 进行比较
        • 举例:s = "abc", t = "ab",按照当前的递推公式 dp[2][1] = dp[2][0] = 0,按照之前的递推公式 dp[2][1] = max(dp[1][1], dp[2][0]) = 2,这两个结果明显不同,但是在比较 dp[2][1] == len(s) 时给出的结果都是 False
        • 举例:s = "ab", t = "abe",按照当前的递推公式 dp[1][2] = dp[1][1] = 2,按照之前的递推公式 dp[2][1] = max(dp[1][1], dp[2][0]) = 2,这两个结果相同,在比较 dp[2][1] == len(s) 时给出的结果都是 True
        • 从这两个例子可以看出,两种递推公式似乎都不影响结果,但 s = "abc", t = "ab" 时得到 dp[2][1] = 0 是明显不符合当前定义的。
    • 所以根据当前定义,当 s[i] != t[j] 时,还是使用 dp[i][j] = max(dp[i][j-1], dp[i-1][j]) 作为递推公式。

      那么在什么定义下可以使用 dp[i][j] = dp[i][j-1]

      • dp[i][j]s[:i+1](必须包括 s[i])和 t[:j+1] 的最大公共子序列的长度
      • 在这个定义下,如果 s[i] != t[j],那么只能删除 t[j] 来得到子问题的状态
  3. dp 数组的初始化:对于当前的定义,初始化和之前是一样的,都是针对第一行和第一列进行单独的初始化

  4. dp 的遍历顺序:从上到下,从左到右。满足当前位置的左上角已经解决即可。

  5. 举例推导:s = "abc", t = "ahbgdc"

    a h b g d c
    a 1 1 1 1 1 1
    b 1 1 2 2 2 2
    c 1 1 2 2 2 3

以下代码解的 ij 的含义是反的,但不影响解题。

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        if len(s) == 0:
            return True
        if len(t) == 0:
            return False

        # dp[i][j] is the max length of common subarray between t[:i+1] and s[:j+1]
        dp = [[0] * len(s) for _ in range(len(t))]

        first_col_flag = False
        for j in range(len(s)):
            if t[0] == s[j]:
                first_col_flag = True
            if first_col_flag:
                dp[0][j] = 1
        first_row_flag = False
        for i in range(len(t)):
            if t[i] == s[0]:
                first_row_flag = True
            if first_row_flag:
                dp[i][0] = 1
        
        for i in range(1, len(t)):
            for j in range(1, len(s)):
                if t[i] == s[j]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        
        return dp[-1][-1] == len(s)

dp - 传递 bool 来确定编辑

上面对于 dp[i][j] = dp[i][j-1] 的递推公式进行了很长的讨论。根据纯粹的最长公共子序列的长度的定义,这个递推公式显得不正常。但是如果用如下的 dp 定义,就能意识到利用这个递推公式的情景。

  1. dp 数组的下标含义:dp[i][j] 记录了 s[:j+1] 是否是 t[:i+1] 的子序列(bool

  2. dp 递推公式:

    • 如果 s[j] == t[i],那么显然可以直接继承之前的状态 dp[i][j] = dp[i-1][j-1]
    • 否则,dp[i][j] = dp[i-1][j]
      • 根据定义中要求判断 s[:j+1],很明显不能利用 dp[i][j-1],因为即使确定 s[:j]t[:i+1] 的子序列,在 s[j] != t[i] 的情况下也不能判断 s[:j+1] 是否是 t[:i+1] 的子序列
      • 而对于 dp[i-1][j],既然 s[j] != t[i],那么 t[i] 就不能在包含子序列这个情况下产生任何作用,自然应该直接继承 dp[i-1][j]
  3. dp 数组的初始化:由于我们在递推中用到了 dp[i-1][j]dp[i-1][j-1],当然需要初始化第一行和第一列

    • 对于 j=0,只要 t[:i+1] 包含了 s[0],第一列接下来所有的值都应该是 True
    • 对于 i>0t[0] 明显没有可能包含 s[:i+1],第一行所有的值都设为 False 即可,也就不需要额外初始化
  4. dp 的遍历顺序:从上到下,从左到右即可。

  5. 举例说明:s = "abc", t = "ahbgdc"

    a h b g d c
    a True True True True True True
    b False False True True True True
    c False False False False False True
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        if len(s) == 0:
            return True
        if len(t) == 0:
            return False

        # dp[i][j] represents whether s[:j+1] is a subsequence of t[:i+1]
        dp = [[False] * len(s) for _ in range(len(t))]

        row_flag = False
        for i in range(len(t)):
            if t[i] == s[0]:
                row_flag = True
            if row_flag:
                dp[i][0] = True
        
        for i in range(1, len(t)):
            for j in range(1, len(s)):
                if t[i] == s[j]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = dp[i-1][j]
            
        return dp[-1][-1]

双指针

最朴实、直观的解法。

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        if len(s) == 0:
            return True

        sub_idx = 0
        for idx in range(len(t)):
            if t[idx] == s[sub_idx]:
                sub_idx += 1
            if sub_idx == len(s):
                return True
        return False

115. 不同的子序列

题目链接 | 解题思路

看到多少种方法 + 子序列,第一反应是回溯所有的子序列,然而数据量很明显不可能支持回溯,题目难度也没有到必须暴力搜索的地步。
如果本题需要的是连续子数组,那么可以考虑 KMP。

  1. dp 数组的下标含义:dp[i][j]s[:i+1] 的所有子序列中和 t[:j+1] 相同的子序列的数量

  2. dp 递推公式:

    • 如果 s[i] != t[j],那么就不能用 s[i] 参与子序列和 t[:j+1]的匹配,dp[i][j] = dp[i-1][j]
    • 如果 s[i] == t[j],那么还要加上利用 s[i] 参与子序列和 t[:j+1]的匹配数量,dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
  3. dp 数组的初始化:根据递推公式的要求,还是要初始化第一行和第一列

    • j=0,则 dp[i][0] 就是 s[:i+1] 中包含的 t[0] 的数量
    • i=0,则 dp[0][j] = 0 ( j > 0 j>0 j>0),因为 s 不可能包含比自己长的子序列,所以不需要额外初始化
  4. dp 遍历顺序:从上到下、从左到下即可

  5. 举例推导:s = "rabbbit", t = "rabbit"

    r a b b i t
    r 1 0 0 0 0 0
    a 1 1 0 0 0 0
    b 1 1 1 0 0 0
    b 1 1 2 1 0 0
    b 1 1 3 3 0 0
    i 1 1 3 3 3 0
    t 1 1 3 3 3 3
class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        # dp[i][j] represents the number of distinct subsequences of s[:i+1] equals to t[:j+1]
        dp = [[0] * len(t) for _ in range(len(s))]

        dp[0][0] = 1 if t[0] == s[0] else 0
        for i in range(1, len(s)):
            dp[i][0] = dp[i-1][0]
            if t[0] == s[i]:
                dp[i][0] += 1
        
        for i in range(1, len(s)):
            for j in range(1, len(t)):
                dp[i][j] = dp[i-1][j]
                if s[i] == t[j]:
                    dp[i][j] += dp[i-1][j-1]
        
        return dp[-1][-1]

你可能感兴趣的:(代码随想录算法训练营一刷,算法)