BM65 最长公共子序列(二)¶
中等通过率 31.88%python3核心代码模式样例通过牛客 AC动态规划
讲解章节:线性 DP
一句话
填出 LCS 表,再从右下角回溯把子序列本身拼出来。
解题思路¶
这题考什么¶
LCS(最长公共子序列,两串各自删去若干字符后能得到的最长相同序列)的标准二维 DP, 外加一步方案回溯:题目要的是子序列本身,不是长度。 这一点直接决定了空间不能压——滚动数组只留一行,回溯需要整张表, O(n*m) 的空间正是题面允许的上界。前置知识见 线性 DP。
状态定义:dp[i][j] = s1 的前 i 个字符与 s2 的前 j 个字符的最长公共子序列长度。 转移方程:
s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
否则: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
dp[0][*] = dp[*][0] = 0 有一方是空串时公共子序列必为空
末尾字符相同就一定可以把它配成一对:任何不用这对字符的方案, 都能改造成用它的方案而长度不减,所以取 dp[i-1][j-1] + 1 不会漏掉最优解。 末尾字符不同则两个末字符至少有一个用不上,扔掉其中一个再取较优的那种扔法。
回溯:从 (n, m) 出发倒着走,每一步问「这个格子的值是怎么来的」。
- 字符相同 -> 这一对字符必定出现在答案里,记下它并同时向左上退一格;
- 否则往 dp 值更大的那一侧退(相等时任选一侧,题面保证最长公共子序列唯一)。
收集顺序是从后往前,最后反转即得答案。
数据规模与复杂度¶
|str1|, |str2| <= 2000,时限 6 秒(其他语言)。 表格 2001 * 2001 约 4e6 格,时间 O(nm)、空间 O(nm),与题面要求的上界吻合。 4e6 次 Python 级内层迭代逼近 CPython 每秒千万次的量级, 所以内层刻意压到最少的操作:行引用 prev / cur 提前取出、 比较字符时用局部变量 c 缓存 s1[i-1],避免每格重复做两次下标寻址。 回溯至多走 n + m = 4000 步,可以忽略不计。
坑在哪¶
- 最长公共子序列为空时要返回字符串 "-1",不是空串、不是 0,也不是 -1 这个整数。 任一输入串为空时同样走这条出口。
- 不能把 dp 压成一维。滚动数组只保留相邻两行,回溯时问不出「这个值是怎么来的」, 方案就还原不回来了。
- 回溯的分支必须与填表时的取舍一致:填表用 max(dp[i-1][j], dp[i][j-1]), 回溯就往值更大的一侧退;两处判据不一致会走到值更小的格子上, 拼出的串比 dp[n][m] 短。
- 回溯拿到的字符是倒序的,漏掉 reverse 会输出整个答案的镜像。
- dp 多开一行一列当哨兵边界,于是 s1 的第 i 个字符是 s1[i-1]。 这个 ±1 的偏移贯穿全表,混用下标会整体错位。
样例复核¶
s1 = "1A2C3D4B56"、s2 = "B1D23A456A",dp[10][10] = 6, 回溯依次收到 6、5、4、3、2、1,反转后是 "123456",与期望输出一致。
参考实现¶
[:octicons-arrow-left-16: BM64](BM64.md) [BM66 :octicons-arrow-right-16:](BM66.md)