好的,我现在随机为你挑选一个线性动态规划领域中,且不在你已提供列表里的算法题目。
线性动态规划:两个字符串的最短公共超序列长度 (Shortest Common Supersequence - Length)
题目描述
给你两个字符串 text1 和 text2,请你找出一个最短的字符串,使得 text1 和 text2 都是这个字符串的 子序列。返回这个最短字符串的 长度。
子序列定义:一个字符串可以通过删除原字符串某些字符(也可以不删除)而不改变剩余字符的相对顺序得到。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是。
示例 1:
- 输入:
text1 = "abac",text2 = "cab" - 输出:
5 - 解释:最短公共超序列之一是
"cabac"。"abac"是"c a b a c"的子序列(标记下划线)。"cab"也是"c a b a c"的子序列。长度为5。
示例 2:
- 输入:
text1 = "aaaaaaaa",text2 = "aaaaaaaa" - 输出:
8 - 解释:最短公共超序列就是它本身。
解题过程循序渐进讲解
我们的目标是求最短公共超序列(SCS)的长度。这个问题和最长公共子序列(LCS)有着非常直接和优美的关系。
第一步:将问题转化为已知问题
让我们思考一下 text1(长度为 n)和 text2(长度为 m)以及它们的最长公共子序列 LCS(长度为 l)。
- 超序列与子序列的关系:一个超序列必须包含
text1和text2的所有字符,同时保持各自原有的顺序。 - 重叠部分:如果
text1和text2有公共的字符(按照顺序),那么在构造超序列时,这些公共字符只需要出现一次,就可以同时满足是text1和text2的子序列的要求。 - 核心观察:这些“只需要出现一次”的字符,正是
text1和text2的最长公共子序列 (LCS)。
第二步:推导长度公式
我们可以这样构建最短公共超序列:
- 先写出
text1和text2的 LCS。 - 对于
text1中不属于 LCS 的字符,我们必须按顺序将它们插入到超序列中 LCS 的对应位置之间或两端。 - 对于
text2中不属于 LCS 的字符,我们也必须按顺序将它们插入。
长度计算:
text1的长度是n,其中l个字符已经在 LCS 里,所以需要额外插入(n - l)个来自text1的独有字符。text2的长度是m,其中l个字符已经在 LCS 里,所以需要额外插入(m - l)个来自text2的独有字符。- 最后,再加上 LCS 本身的
l个字符。
因此,最短公共超序列的长度 = (n - l) + (m - l) + l = n + m - l。
结论:SCS长度 = text1长度 + text2长度 - LCS长度。
第三步:设计动态规划状态
既然我们已经知道关键在于求 LCS长度,而这是一个经典的动态规划问题。
我们定义 dp[i][j]:表示 text1 的前 i 个字符(下标 0 到 i-1)和 text2 的前 j 个字符(下标 0 到 j-1)的 最长公共子序列的长度。
i的取值范围是[0, n]j的取值范围是[0, m]dp[0][j]表示text1取空字符串,所以 LCS 长度为 0。dp[i][0]同理。
我们的最终目标是 dp[n][m],即两个完整字符串的 LCS 长度 l。
第四步:建立状态转移方程
我们考虑 text1[i-1] 和 text2[j-1] 这两个字符(因为下标从0开始)。
-
如果两个字符相等:
- 这个字符必然是 LCS 的一部分。
- 那么,
text1前i-1个字符和text2前j-1个字符的 LCS 长度加上这个字符,就构成了新的 LCS。 - 所以,
dp[i][j] = dp[i-1][j-1] + 1。
-
如果两个字符不相等:
- 那么,
text1[i-1]和text2[j-1]不可能同时出现在当前的 LCS 中。 - 我们有两种选择:
a) 不考虑text1[i-1],看text1前i-1个字符和text2前j个字符的 LCS。
b) 不考虑text2[j-1],看text1前i个字符和text2前j-1个字符的 LCS。 - 当前的 LCS 长度应该是这两种选择中的最大值(因为我们要找最长的)。
- 所以,
dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
- 那么,
转移方程总结:
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
第五步:确定初始条件
dp[0][j] = 0,空字符串和任何字符串的 LCS 长度为 0。dp[i][0] = 0,同理。
第六步:计算顺序与最终答案
我们从小规模问题向大规模问题计算:
- 使用两层循环,
i从 1 到n,j从 1 到m。 - 计算完整个
dp表后,l = dp[n][m]。 - 最终答案:
SCS长度 = n + m - l。
第七步:用示例验证
以 text1 = "abac" (n=4), text2 = "cab" (m=3) 为例。
-
构建
dp表(5行 x 4列,包含空串情况):dp[i][j] 表 (i行,j列): '' 'c' 'a' 'b' '' 0 0 0 0 'a' 0 0 1 1 'b' 0 0 1 2 'a' 0 0 1 2 'c' 0 1 1 2计算过程(逐步):
i=1, j=1:'a' != 'c',dp[1][1] = max(dp[0][1], dp[1][0]) = max(0, 0) = 0i=1, j=2:'a' == 'a',dp[1][2] = dp[0][1] + 1 = 0 + 1 = 1i=2, j=3:'b' == 'b',dp[2][3] = dp[1][2] + 1 = 1 + 1 = 2i=4, j=1:'c' == 'c',dp[4][1] = dp[3][0] + 1 = 0 + 1 = 1- 等等。
-
dp[4][3] = 2,所以l = 2。 -
最短公共超序列长度 =
n + m - l = 4 + 3 - 2 = 5。与示例一致。
第八步:复杂度分析
- 时间复杂度:我们需要填充一个
(n+1) x (m+1)的dp表,每次操作是常数时间。所以总时间复杂度为 O(n * m)。 - 空间复杂度:如果使用完整的二维数组,是 O(n * m)。可以观察到状态转移只依赖于上一行和当前行的左边,所以可以优化到 O(min(n, m))。
核心要点总结
这道题的巧妙之处在于,它将“最短公共超序列”问题,转化为了经典的“最长公共子序列”问题。
核心公式:
最短公共超序列长度 = 字符串A长度 + 字符串B长度 - 最长公共子序列长度
你只需要熟练解决 LCS 的动态规划,就能轻松解决 SCS 的长度问题。如果需要输出具体的超序列字符串,则需要在动态规划过程中记录路径(使用额外的数组存储选择方向),然后在回溯时根据 dp 值和字符相等关系来构造。