LeetCode 1092. 最短公共超序列 是一道困难题。题目要求我们给定两个字符串 str1
和 str2
,返回一个最短的字符串,使得 str1
和 str2
都是它的子序列。如果答案有多个,可以返回任意一个。
str1
: 第一个字符串,仅包含小写英文字母。str2
: 第二个字符串,仅包含小写英文字母。str1
和 str2
都是它的子序列。t
中删除一些字符(也可以不删除),可以得到字符串 s
,则 s
是 t
的子序列。1 <= str1.length, str2.length <= 1000
str1
和 str2
只包含小写英文字母。示例 1:
str1 = "abac", str2 = "cab"
"cabac"
"cabac"
中的第一个 'c'
,得到 "abac"
,符合 str1
。"cabac"
中的末尾 "ac"
,得到 "cab"
,符合 str2
。"cabac"
是满足条件的最短字符串之一。示例 2:
str1 = "aaaaaaaa", str2 = "aaaaaaaa"
"aaaaaaaa"
为了方便描述,我们将 str1
记作 s
,str2
记作 t
。问题的核心是构造一个最短的字符串,既包含 s
的所有字符(按顺序),也包含 t
的所有字符(按顺序)。我们可以从递归入手,逐步优化到动态规划。
考虑从后往前构造答案。对于 s = "abac"
和 t = "cab"
,答案的最后一个字符是什么?
s
的最后一个字符 'c'
,问题变成构造 s[:-1] = "aba"
和 t = "cab"
的最短公共超序列。t
的最后一个字符 'b'
,问题变成构造 s = "abac"
和 t[:-1] = "ca"
的最短公共超序列。s
和 t
的最后一个字符相同(例如都为 'a'
),则这个字符一定是答案的最后一个字符,问题变成构造两者的前缀。分类讨论:
s
是空串,返回 t
。t
是空串,返回 s
。s[-1]
(或 t[-1]
,两者相同)加上 s[:-1]
和 t[:-1]
的最短公共超序列。s[:-1]
和 t
的答案(ans1
),加上 s[-1]
。s
和 t[:-1]
的答案(ans2
),加上 t[-1]
。ans1
和 ans2
中较短的一个。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)
,指数级别不可接受。为了避免重复计算,我们可以用记忆化搜索优化递归。用下标 i
和 j
表示 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))
,存储字符串结果占用较多空间。我们可以先计算长度,再构造答案:
dfs(i, j)
返回长度,而不是字符串。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]
表示 s
前 i
个字符和 t
前 j
个字符的最短公共超序列长度。
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] = i
,f[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)
。s = "abac", t = "cab"
"cabac"
s = "aaaaaaaa", t = "aaaaaaaa"
"aaaaaaaa"
从朴素递归到记忆化搜索,再到动态规划,我们逐步优化了时间和空间复杂度。这道题的关键在于理解子问题递归关系,并通过 DP 高效构造答案。希望这篇博客能帮助你掌握双序列动态规划的思考过程!