3.8 最长公共子序列算法
这一篇讲动态规划在字符串上的经典应用——最长公共子序列(Longest Common Subsequence,LCS)。它是生物信息学(DNA 序列比对)、版本控制(diff)、拼写纠错的基础。它还确立了一个极常用的 DP 模式:双序列 DP(状态用两个序列的下标 刻画)。
1.问题:两个序列最长的共同子序列
给两个序列 和 ,找它们最长的公共子序列。子序列不要求连续(区别于子串),只要求相对顺序一致。
比如 、,一个 LCS 是 (长度 4)。
朴素枚举: 有 个子序列,对每个检查是否为 的子序列——爆炸。但子问题高度重叠,DP 出场。
2.状态与转移
状态: = 的前 个字符与 的前 个字符的 LCS 长度。
转移:看 和 这两个"末尾字符":
- 若 (末尾相同):这个字符一定可以接到 LCS 末尾,。
- 若 :末尾不同,LCS 要么不含 、要么不含 ,取较大:。
基底 (空序列的 LCS 长度 0)。
3.填表与还原
填 的表, 依赖左、上、左上,所以从左到右、从上到下填。时间空间各 。
要还原具体是哪个子序列(而不只是长度),填表时记一张决策表:每个格记"这个值来自左上(+1)、左、还是上",最后从 回溯到 即可还原一条 LCS。
4.这个转移为什么对
值得想清楚 时为什么取 。直觉: 和 至少有一个不在 LCS 里(因为它俩不同,不可能同时在末尾)。如果 不在,问题退化成 前 个 vs 前 个(即 );如果 不在,退化成 。两者取大即可。关键是"末尾字符是否匹配"决定了最优子结构怎么切分——匹配则共享末尾、不匹配则丢掉其一。
5.双序列 DP 的普适模式
LCS 确立了双序列 DP这个模式:状态 是"两个序列各看前 、前 个",转移看末尾字符的关系。很多问题复用它:
- 编辑距离(Levenshtein):把 变成 最少要几次插入/删除/替换。转移更复杂(三种操作取最小),但状态结构同 LCS。
- 最长公共子串(要求连续):转移在失配时清零而非取 max。
- 序列比对(带打分):生物信息学里 DNA/蛋白质比对,是 LCS 的加权推广。
识别出"两个序列、状态用各自下标"这个特征,就往双序列 DP 靠。它是 DP 三大典型结构之一(线性、区间、双序列)。
6.练习
Q1. 、,简述 表怎么填出 LCS 长度 4。
按 则 、否则 填。每当末尾字符匹配(如两个 B、两个 A、两个 B),LCS 长度沿对角线 +1。最终 。回溯决策表得一条 LCS 如 BCAB 或 BDAB。
Q2. LCS 转移里, 时为什么取 而不是别的?
因为 ,它俩不可能同时在 LCS 末尾,至少一个不在。若 不在,最优就是 前 vs 前 的 LCS,即 ;若 不在,是 。两者涵盖所有可能,取较大。关键:末尾是否匹配决定子结构切分——匹配共享末尾,失配丢掉其一。
Q3.(思考题) LCS 是"双序列 DP"的代表。这个模式还能解什么问题?状态结构有什么共同点?
双序列 DP 状态都是 = 两个序列各看前 、前 的最优。共同点:转移都看两序列末尾字符的关系(匹配/失配),据此决定共享末尾还是丢其一。编辑距离、序列比对、最长公共子串都复用这个骨架,只是转移里"操作"不同(LCS 是+1/max,编辑距离是 min+代价)。识别"两序列+末尾字符关系"就往双序列 DP 靠。
7.小结
最长公共子序列用双序列 DP,状态 看两序列前缀,转移由末尾字符匹配与否决定, 求解并可回溯还原。它确立了"双序列 DP"模式(编辑距离、序列比对都复用)。下一篇是第三卷的收尾,也是 DP 的极限挑战——指数级的 Held-Karp 旅行商。