LeetCode 1092:最短公共超序列


LeetCode 1092:最短公共超序列

题目描述

LeetCode 1092. 最短公共超序列 是一道困难题。题目要求我们给定两个字符串 str1str2,返回一个最短的字符串,使得 str1str2 都是它的子序列。如果答案有多个,可以返回任意一个。

题目详情

  • 输入
    • str1: 第一个字符串,仅包含小写英文字母。
    • str2: 第二个字符串,仅包含小写英文字母。
  • 输出
    • 一个最短的字符串,使得 str1str2 都是它的子序列。
  • 子序列定义
    • 如果从字符串 t 中删除一些字符(也可以不删除),可以得到字符串 s,则 st 的子序列。
  • 提示
    • 1 <= str1.length, str2.length <= 1000
    • str1str2 只包含小写英文字母。

示例

  1. 示例 1

    • 输入:str1 = "abac", str2 = "cab"
    • 输出:"cabac"
    • 解释:
      • 删除 "cabac" 中的第一个 'c',得到 "abac",符合 str1
      • 删除 "cabac" 中的末尾 "ac",得到 "cab",符合 str2
      • "cabac" 是满足条件的最短字符串之一。
  2. 示例 2

    • 输入:str1 = "aaaaaaaa", str2 = "aaaaaaaa"
    • 输出:"aaaaaaaa"
    • 解释:两字符串相同,直接返回即可。

解题思路

为了方便描述,我们将 str1 记作 sstr2 记作 t。问题的核心是构造一个最短的字符串,既包含 s 的所有字符(按顺序),也包含 t 的所有字符(按顺序)。我们可以从递归入手,逐步优化到动态规划。

一、初步思路:递归

考虑从后往前构造答案。对于 s = "abac"t = "cab",答案的最后一个字符是什么?

  • 如果是 s 的最后一个字符 'c',问题变成构造 s[:-1] = "aba"t = "cab" 的最短公共超序列。
  • 如果是 t 的最后一个字符 'b',问题变成构造 s = "abac"t[:-1] = "ca" 的最短公共超序列。
  • 如果 st 的最后一个字符相同(例如都为 'a'),则这个字符一定是答案的最后一个字符,问题变成构造两者的前缀。

分类讨论:

  1. 边界条件
    • 如果 s 是空串,返回 t
    • 如果 t 是空串,返回 s
  2. s[-1] == t[-1]
    • 答案是 s[-1](或 t[-1],两者相同)加上 s[:-1]t[:-1] 的最短公共超序列。
  3. s[-1] != t[-1]
    • 计算 s[:-1]t 的答案(ans1),加上 s[-1]
    • 计算 st[:-1] 的答案(ans2),加上 t[-1]
    • ans1ans2 中较短的一个。
递归代码(会超时)
class Solution:
    def shortestCommonSupersequence(self, s: str, t: str) -> str:
        if not s: return t
        if not t: return s
        if s[-1] == t[-1]:
            return self.shortestCommonSupersequence(s[:-1], t[:-1]) + s[-1]
        ans1 = self.shortestCommonSupersequence(s[:-1], t) + s[-1]
        ans2 = self.shortestCommonSupersequence(s, t[:-1]) + t[-1]
        return ans1 if len(ans1) < len(ans2) else ans2
  • 问题:存在大量重复子问题,例如 s = "ab"t = "cd" 的计算会被多次调用。
  • 时间复杂度O((n+m) * 2^(n+m)),其中 n = len(s)m = len(t),指数级别不可接受。

二、记忆化搜索:初步优化

为了避免重复计算,我们可以用记忆化搜索优化递归。用下标 ij 表示 s 的前 i 个字符和 t 的前 j 个字符的最短公共超序列。

优化代码
class Solution:
    def shortestCommonSupersequence(self, s: str, t: str) -> str:
        @cache
        def dfs(i: int, j: int) -> str:
            if i < 0: return t[:j+1]
            if j < 0: return s[:i+1]
            if s[i] == t[j]:
                return dfs(i-1, j-1) + s[i]
            ans1 = dfs(i-1, j) + s[i]
            ans2 = dfs(i, j-1) + t[j]
            return ans1 if len(ans1) < len(ans2) else ans2
        return dfs(len(s)-1, len(t)-1)
  • 改进:用 @cache 缓存每个状态 (i, j) 的结果,避免重复计算。
  • 时间复杂度O(nm(n+m)),状态数为 O(nm),每次计算拼接字符串需 O(n+m)
  • 空间复杂度O(nm(n+m)),存储字符串结果占用较多空间。
  • 问题:仍然超时,因为字符串拼接和存储开销过大。

三、记忆化搜索:进一步优化

我们可以先计算长度,再构造答案:

  1. dfs(i, j) 返回长度,而不是字符串。
  2. make_ans(i, j) 根据长度递归构造答案。
优化代码
class Solution:
    def shortestCommonSupersequence(self, s: str, t: str) -> str:
        @cache
        def dfs(i: int, j: int) -> int:
            if i < 0: return j + 1
            if j < 0: return i + 1
            if s[i] == t[j]:
                return dfs(i-1, j-1) + 1
            return min(dfs(i-1, j), dfs(i, j-1)) + 1

        def make_ans(i: int, j: int) -> str:
            if i < 0: return t[:j+1]
            if j < 0: return s[:i+1]
            if s[i] == t[j]:
                return make_ans(i-1, j-1) + s[i]
            if dfs(i, j) == dfs(i-1, j) + 1:
                return make_ans(i-1, j) + s[i]
            return make_ans(i, j-1) + t[j]

        return make_ans(len(s)-1, len(t)-1)
  • 时间复杂度O(nm)(计算长度)+ O(n+m)(构造答案)。
  • 空间复杂度O(nm)(存储长度)。
  • 改进:长度计算高效,构造答案的递归是一条链。

四、动态规划:递推 + 迭代

将记忆化搜索改为递推,用二维数组 f[i+1][j+1] 表示 si 个字符和 tj 个字符的最短公共超序列长度。

状态转移方程
  • 如果 s[i] == t[j]f[i+1][j+1] = f[i][j] + 1
  • 如果 s[i] != t[j]f[i+1][j+1] = min(f[i][j+1], f[i+1][j]) + 1
  • 边界:f[i][0] = if[0][j] = j
最终代码
class Solution:
    def shortestCommonSupersequence(self, s: str, t: str) -> str:
        n, m = len(s), len(t)
        f = [[0] * (m + 1) for _ in range(n + 1)]
        for j in range(m + 1):
            f[0][j] = j
        for i in range(n + 1):
            f[i][0] = i
        
        for i in range(n):
            for j in range(m):
                if s[i] == t[j]:
                    f[i+1][j+1] = f[i][j] + 1
                else:
                    f[i+1][j+1] = min(f[i][j+1], f[i+1][j]) + 1
        
        ans = []
        i, j = n - 1, m - 1
        while i >= 0 or j >= 0:
            if i < 0:
                ans.append(t[j])
                j -= 1
            elif j < 0:
                ans.append(s[i])
                i -= 1
            elif s[i] == t[j]:
                ans.append(s[i])
                i -= 1
                j -= 1
            elif f[i+1][j+1] == f[i][j+1] + 1:
                ans.append(s[i])
                i -= 1
            else:
                ans.append(t[j])
                j -= 1
        return ''.join(ans[::-1])
  • 时间复杂度O(nm)(填表)+ O(n+m)(构造答案)。
  • 空间复杂度O(nm)

测试用例验证

示例 1

  • 输入:s = "abac", t = "cab"
  • 输出:"cabac"
  • 验证:符合要求,且长度最短。

示例 2

  • 输入:s = "aaaaaaaa", t = "aaaaaaaa"
  • 输出:"aaaaaaaa"
  • 验证:直接返回原串,正确。

总结

从朴素递归到记忆化搜索,再到动态规划,我们逐步优化了时间和空间复杂度。这道题的关键在于理解子问题递归关系,并通过 DP 高效构造答案。希望这篇博客能帮助你掌握双序列动态规划的思考过程!


你可能感兴趣的:(每日算法,leetcode,算法,职场和发展)