LC1143 最长公共子序列¶
讲解章节:线性 DP
一句话
末尾字符相同就一起吃掉并加一,不同则丢弃其中一串的末位取较优。
解题思路¶
这题考什么¶
LCS(Longest Common Subsequence,最长公共子序列)是双序列 DP 的教科书例题, 见 线性 DP。子序列允许跳过字符但不能重排, 所以两串的比较必须从末尾同步推进,这样才有「谁被消费掉」的清晰分类。
状态定义:f[i][j] 表示 text1 的前 i 个字符与 text2 的前 j 个字符的最长公共子序列长度。 边界:f[0][j] = f[i][0] = 0,任一方为空时公共子序列只能是空。 转移方程,记 a = text1 的第 i 个字符、b = text2 的第 j 个字符:
两条式子的含义:
相同时,把 a 与 b 配成一对接在「两串各去掉末位」的最优解后面。
这里**不需要**再与另外两项取最大——配上这一对永远不吃亏,
因为任何不含这一对的方案都能被改造成含它且不更短的方案。
不同时,a 与 b 至少有一个不可能出现在公共子序列的末尾,
于是分两种情况丢掉其中一个末位,取较优者。
要返回的是长度,不是子序列本身。这一点与牛客 BM65(最长公共子序列(二)) 恰好相反——那题要求输出具体的子串,必须额外记录转移方向再回溯还原。 只求长度时,回溯所需的信息可以整个丢掉,这正是下面能把二维表滚成一行的前提。
滚动成一行时的陷阱与 LC72 编辑距离完全相同:扫到第 j 列的瞬间, dp[j] 尚未覆盖是「上」,dp[j-1] 已覆盖是「左」,而「左上」在 dp[j-1] 被写入时就丢了,必须用临时变量 diag 在覆盖之前接住, 每列算完再把本列的「上」交棒给下一列的「左上」。
数据规模与复杂度¶
1 <= text1.length, text2.length <= 1000,仅小写英文字母。 时间 O(m * n) 最坏 1e6 次内层迭代,本机实测 1000 与 1000 对拼约 0.08 秒。 空间 O(min(m, n)):把较短的串放到内层维度上,滚动数组最长 1001 项。 暴力枚举 text1 的全部子序列再逐个检查是 2 的 1000 次方 种,完全不可行。
坑在哪¶
- 返回长度不是子序列。看到「最长公共子序列」就去拼字符串是最常见的跑偏, 本题的判题拿到字符串会直接判类型错。
- diag 每行都要重置为 0。本行第一格的左上角是 f[i-1][0],恒等于 0 (text2 取空前缀时 LCS 为 0),所以进入内层前直接置 0 即可, 不必像 LC72 那样先接住 dp[0] 再改写——这里 dp[0] 全程不变。 忘记重置会把上一行末尾的值当成本行的左上角,答案偏大。
- up 必须在 dp[j] 被覆盖之前取出,否则读到的是刚写进去的新值。
- 每列结束时 diag = up。漏掉这一步,diag 会一直停在 0, 所有匹配都只会算出长度 1。
- 不匹配时只在「左比上大」的情况下才赋值,else 分支可以省略—— 此时 dp[j] 原样保留着 up,正是「丢掉 text1 的这一位」那个候选。 这不是偷懒,而是滚动数组的自然结果:不写就等于取了 up。
- 相同时是 diag + 1,不能写成 max(dp[j-1], dp[j]) + 1。 后者可能把同一个字符重复计入,例如 text1 = "aa"、text2 = "a" 会算出 2,而正确答案是 1。
- 交换两串是安全的:LCS 对称,交换只为把滚动数组压到 min(m, n)。
样例复核¶
text1 = "abcde"、text2 = "ace"。text1 本就更长,不触发交换,内层维度是长度 3 的 "ace"。
dp 初值 [0, 0, 0, 0]
a='a':'a'=='a' -> dp[1] = 0 + 1 = 1 -> dp = [0, 1, 1, 1]
a='b':无匹配,全靠左右传递 -> dp = [0, 1, 1, 1]
a='c':'c'=='c' 在第 2 列 -> dp[2] = diag + 1 = 1 + 1 = 2 -> dp = [0, 1, 2, 2]
a='d':无匹配 -> dp = [0, 1, 2, 2]
a='e':'e'=='e' 在第 3 列 -> dp[3] = 2 + 1 = 3 -> dp = [0, 1, 2, 3]
返回 dp[3] = 3,对应公共子序列 "ace",与期望输出一致。