与序列 DP 状态设计)
教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载导读本文以「宫水三叶的刷题日记」系列仓库LogicStack-LeetCode中的 1143. 最长公共子序列中等 题解为核心系统讲解最长公共子序列LCS这一序列 DP 经典模型的完整推导过程、两种主流实现技巧追加空格与下标偏移、以及四语言Java / C / Python / TypeScript可运行代码。读完本文你将掌握 LCS 的「状态定义 → 转移方程 → 边界初始化 → 复杂度分析」全流程并能将其迁移到 583、1035、1092 等同源变体题在仓库的 序列 DP 索引 中看到完整题单。题目背景与题意理解题目描述给定两个字符串s1和s2返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列返回0。子序列的定义一个字符串的子序列是由原字符串在不改变字符相对顺序的情况下删除某些字符也可以不删除任何字符后组成的新字符串。例如ace是abcde的子序列但aec不是——因为a、e、c在原串中的相对顺序被打乱了。两个字符串的公共子序列是这两个字符串所共同拥有的子序列。样例分析输入输出解释s1 abcde,s2 ace3最长公共子序列是ace长度为 3s1 abc,s2 abc3最长公共子序列是abc长度为 3s1 abc,s2 def0两个字符串没有公共子序列数据范围本题约束1 s1.length, s2.length 1000s1和s2仅由小写英文字符组成本题被收录在仓库 LeetCode/1141-1150/1143. 最长公共子序列中等.mdTag 为「最长公共子序列 / LCS / 序列 DP」是一道 LCS 的裸题也是整个序列 DP 知识体系中最重要的基础模型之一。核心思想状态定义与转移方程推导状态定义LCS 是双序列 DP 的典型代表。题解中给出的状态定义为f[i][j]代表考虑s1的前i个字符、考虑s2的前j个字符形成的最长公共子序列长度。这里需要特别强调一个关键语义「考虑前i个字符」并不要求「必须包含第i个字符」。这与最长上升子序列LIS中f[i]通常表示「以第i个元素结尾」的状态定义有本质区别。LCS 的状态只是刻画了一个「前缀窗口」至于窗口最后一个字符是否被选中进入公共子序列完全由转移方程决定。转移方程有了状态定义转移方程可自然推出当s1[i] s2[j]时f[i][j] f[i-1][j-1] 1含义必然使用s1[i]与s2[j]作为公共子序列的最后一个字符在此基础上往前追溯到s1的前i-1个字符与s2的前j-1个字符的 LCS 长度加 1。当s1[i] ! s2[j]时f[i][j] max(f[i-1][j], f[i][j-1])含义分两种情况取最大值——f[i-1][j]必然不使用s1[i]但可能使用s2[j]f[i][j-1]必然不使用s2[j]但可能使用s1[i]。关于「重复状态比较」的深入理解细心的读者会发现在s1[i] ! s2[j]时f[i-1][j]与f[i][j-1]的取 max 操作实际上把「既不使用s1[i]也不使用s2[j]」这一情况即f[i-1][j-1]重复参与了比较。这一点在仓库同系列题解 1035. 不相交的线中等 中有更系统的展开该题把四个候选情况——「不含 s1[i] 且不含 s2[j]」「含 s1[i] 且含 s2[j]」「不含 s1[i] 但含 s2[j]」「含 s1[i] 但不含 s2[j]」逐一分析最终指出f[i-1][j]实际上是「必不含 s1[i]」情况与「都不含」情况的合集但由于求的是最大值只要保证「不漏」即可保证答案正确重复比较不影响正确性。因此 LCS 的转移可以放心地写成上述简洁形式这也是很多同学能 AC 但未必理解透彻的点。在 1035 题解中作者给出更完整的转移方程包含f[i-1][j-1] 1的三者取 max 形式可作为加深理解的对照阅读材料。解法一动态规划追加空格技巧思路与编码细节题解作者习惯在字符串头部追加一个空格目的有二使下标从1开始f[i][0]、f[0][j]自然成为合法的边界状态很容易构造出「可滚动的有效值」——即边界上天然存在一个空字符参与的公共子序列。初始化时f[i][0] 1对 i 0..n f[0][j] 1对 j 0..m注意此时f[0][0] 1表示两个「空格哨兵」匹配产生的 1 个长度最终答案需要减去这个哨兵即返回f[n][m] - 1。题解中同时注释说明for (int i 0; i n; i) Arrays.fill(f[i], 1);这种整体填充 1 的初始化方式同样可行因为空格的引入第一行第一列都会被覆盖为合理值两种初始化方式均可。Java 实现class Solution { public int longestCommonSubsequence(String s1, String s2) { int n s1.length(), m s2.length(); s1 s1; s2 s2; char[] cs1 s1.toCharArray(), cs2 s2.toCharArray(); int[][] f new int[n 1][m 1]; // 因为有了追加的空格我们有了显然的初始化值以下两种初始化方式均可 // for (int i 0; i n; i) Arrays.fill(f[i], 1); for (int i 0; i n; i) f[i][0] 1; for (int j 0; j m; j) f[0][j] 1; for (int i 1; i n; i) { for (int j 1; j m; j) { if (cs1[i] cs2[j]) f[i][j] f[i - 1][j - 1] 1; else f[i][j] Math.max(f[i - 1][j], f[i][j - 1]); } } // 减去最开始追加的空格 return f[n][m] - 1; } }C 实现class Solution { public: int longestCommonSubsequence(string s1, string s2) { int n s1.size(), m s2.size(); s1 s1, s2 s2; int f[n1][m1]; memset(f, 0, sizeof(f)); for(int i 0; i n; i) f[i][0] 1; for(int j 0; j m; j) f[0][j] 1; for(int i 1; i n; i) { for(int j 1; j m; j) { if(s1[i] s2[j]) f[i][j] max(f[i-1][j-1] 1, max(f[i-1][j], f[i][j-1])); else f[i][j] max(f[i-1][j], f[i][j-1]); } } return f[n][m] - 1; } };注C 版本中s1[i] s2[j]分支里多取一次max(f[i-1][j], f[i][j-1])不影响结果因为字符相等时f[i-1][j-1] 1 f[i-1][j]与f[i][j-1]恒成立这里只是为了展示「三者取 max」的等价写法。Python 实现class Solution: def longestCommonSubsequence(self, s1: str, s2: str) - int: n, m len(s1), len(s2) s1, s2 s1, s2 f [[0] * (m 1) for _ in range(n 1)] for i in range(n 1): f[i][0] 1 for j in range(m 1): f[0][j] 1 for i in range(1, n 1): for j in range(1, m 1): if s1[i] s2[j]: f[i][j] f[i - 1][j - 1] 1 else: f[i][j] max(f[i - 1][j], f[i][j - 1]) return f[n][m] - 1TypeScript 实现function longestCommonSubsequence(s1: string, s2: string): number { const n s1.length, m s2.length; s1 s1; s2 s2; const f Array.from({ length: n 1 }, () Array(m 1).fill(0)); for (let i 0; i n; i) f[i][0] 1; for (let j 0; j m; j) f[0][j] 1; for (let i 1; i n; i) { for (let j 1; j m; j) { if (s1[i] s2[j]) f[i][j] f[i - 1][j - 1] 1; else f[i][j] Math.max(f[i - 1][j], f[i][j - 1]); } } return f[n][m] - 1; }复杂度时间复杂度O(n × m)双层循环遍历整个 DP 表空间复杂度O(n × m)二维数组f。解法二动态规划利用下标偏移「追加空格」只是作者个人的编码习惯。事实上完全可以不修改原字符串仅通过微调状态定义实现同样的递推这也是社区中最主流的写法。调整后的状态定义f[i][j]代表考虑s1的前i - 1个字符、考虑s2的前j - 1个字符形成的最长公共子序列长度。此时最终f[n][m]就是答案f[0][0]当作无效值不处理即保持 0 即可。转移时把比较逻辑整体平移一位s1[i-1] s2[j-1]f[i][j] f[i-1][j-1] 1代表使用s1[i-1]与s2[j-1]形成 LCSs1[i-1] ! s2[j-1]f[i][j] max(f[i-1][j], f[i][j-1])代表不使用s1[i-1]与不使用s2[j-1]两种情况中的最大值。Java 实现class Solution { public int longestCommonSubsequence(String s1, String s2) { int n s1.length(), m s2.length(); char[] cs1 s1.toCharArray(), cs2 s2.toCharArray(); int[][] f new int[n 1][m 1]; for (int i 1; i n; i) { for (int j 1; j m; j) { if (cs1[i - 1] cs2[j - 1]) f[i][j] f[i - 1][j - 1] 1; else f[i][j] Math.max(f[i - 1][j], f[i][j - 1]); } } return f[n][m]; } }C 实现class Solution { public: int longestCommonSubsequence(string s1, string s2) { int m s1.size(), n s2.size(); vectorvectorint f(m 1, vectorint(n 1, 0)); for(int i 1; i m; i){ for(int j 1; j n; j){ if(s1[i - 1] s2[j - 1]) f[i][j] f[i - 1][j - 1] 1; else f[i][j] max(f[i - 1][j], f[i][j - 1]); } } return f[m][n]; } };Python 实现class Solution: def longestCommonSubsequence(self, s1: str, s2: str) - int: m, n len(s1), len(s2) f [[0] * (n 1) for _ in range(m 1)] for i in range(1, m 1): for j in range(1, n 1): if s1[i - 1] s2[j - 1]: f[i][j] f[i - 1][j - 1] 1 else: f[i][j] max(f[i - 1][j], f[i][j - 1]) return f[m][n]TypeScript 实现function longestCommonSubsequence(s1: string, s2: string): number { const n s1.length, m s2.length; const cs1 s1.split(), cs2 s2.split(); const f: number[][] Array.from({ length: n 1 }, () Array(m 1).fill(0)); for (let i 1; i n; i) { for (let j 1; j m; j) { if (cs1[i - 1] cs2[j - 1]) f[i][j] f[i - 1][j - 1] 1; else f[i][j] Math.max(f[i - 1][j], f[i][j - 1]); } } return f[n][m]; }复杂度与解法一相同时间O(n × m)空间O(n × m)。两种写法对比维度追加空格技巧下标偏移技巧是否修改原字符串是 s否下标对应关系f[i][j]直接对应s1[i]、s2[j]f[i][j]对应s1[i-1]、s2[j-1]边界初始化需要把f[i][0]、f[0][j]设为 1全 0 即可f[0][0]作无效值最终答案f[n][m] - 1直接f[n][m]滚动数组优化把空间降到 O(m)两种解法都用了O(n × m)的二维数组。但观察转移方程可以发现f[i][j]只依赖f[i-1][j-1]、f[i-1][j]、f[i][j-1]三个「上一行 当前行左侧」的状态因此可以用两行甚至一行加一个临时变量滚动复用将空间复杂度优化到O(m)。以「下标偏移」写法为例滚动数组的核心是pre保存上一行的值对应f[i-1][*]cur表示当前行计算cur[j]时cur[j-1]已经是当前行左侧的最新值pre[j-1]恰好是左上角f[i-1][j-1]。这一优化在n、m较大如 1000 甚至更大时能显著减少内存占用是竞赛与工程中常用的空间优化手段可作为对原题解的进阶补充练习。从本题到整个序列 DP 体系仓库中的同源变体LCS 作为序列 DP 的「母题」在仓库中衍生出大量变体题全部收录于 序列 DP 索引。以下是与本题关系最直接的几道推荐按顺序刷1. 583. 两个字符串的删除操作中等题解位于 LeetCode/581-590/583. 两个字符串的删除操作中等.mdTag 同样包含「最长公共子序列 / 序列 DP」。LCS 转化法设两串长度分别为n、mLCS 长度为max则最小删除步数为n - max m - max删掉两串各自不属于 LCS 的字符。直接序列 DP 法更契合题意的定义是f[i][j]表示「使s1前i个字符与s2前j个字符相同的最小删除次数」转移为字符相等时f[i][j] f[i-1][j-1]不等时f[i][j] min(f[i-1][j] 1, f[i][j-1] 1)。这道题完美展示了「同一个 DP 模型既可以被 LCS 套用也可以直接定义更贴合题意的状态」。2. 1035. 不相交的线中等题解位于 LeetCode/1031-1040/1035. 不相交的线中等.md被作者称为「LCS 的轻度变形题」两排数字之间连线要求数值相等且不相交最大连线数恰好等于 LCS 长度。该题解的精华在于对四种包含/不包含情况的完整分类讨论详见上文「重复状态比较」一节并给出包含f[i-1][j-1] 1的完整转移方程f[i][j] max(f[i-1][j], f[i][j-1]) s1[i] ! s2[j] f[i][j] max(f[i-1][j], f[i][j-1], f[i-1][j-1]1)s1[i] s2[j]强烈建议在刷完 1143 后精读此篇补全对 LCS 转移「不漏即可」的直觉。3. 1092. 最短公共超序列困难题解位于 LeetCode/1091-1100/1092. 最短公共超序列困难.md是 LCS 从「求长度」升级为「求具体方案」的代表先照常预处理f数组再用双指针从(n, m)倒推字符相等属于 LCS时只追加一次并i--, j--不等时根据f[i][j]是从f[i-1][j]还是f[i][j-1]转移而来决定追加s1[i]还是s2[j]最后翻转字符串得到最短公共超序列。这构成了「LCS 求具体方案 构造」的完整方法论。4. 其他延伸在 序列 DP 索引 中还可以看到 1218. 最长定差子序列、1713. 得到子序列的最少操作次数、1537. 最大得分 等进阶序列 DP 题目它们或是 LCS 与 LIS 的结合或是在 LCS 模型上叠加贪心/二分等技巧构成了完整的刷题路径。总结与刷题建议回到本题1143. 最长公共子序列 的价值在于确立双序列 DP 的标准范式状态定义「考虑前 i / 前 j 个字符」、转移方程「相等 1 / 不等取 max」、复杂度O(n×m)这套模板可以直接迁移到大量题目理解「漏而不重」的最值 DP 哲学对于求最大值的问题转移状态只要「不漏」即可保证正确重复比较不影响答案——这一认知能帮你在面对复杂状态设计时避免过度纠结两种编码技巧各有用武之地追加空格适合需要快速构造边界的场景如配合滚动数组初始化下标偏移则是社区默认写法二者都应熟练掌握。推荐刷题路线1143本题→ 583LCS 转化 / 直接 DP→ 1035LCS 变形 状态分类讨论→ 1092LCS 求具体方案完成后可在仓库 序列 DP 索引 中按「推荐指数」继续扩展逐步构建完整的序列 DP 知识体系。赞分享教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载相关推荐从「不相交的线」看透最长公共子序列LCS的本质LogicStack-LeetCode 序列 DP 精讲从「不相交的线」看透最长公共子序列LCS的本质LogicStack LeetCode 序列 DP 精讲 本文是 LogicStack LeetCode 仓教程文档LeetCode-Go 题解精讲1143. Longest Common Subsequence最长公共子序列二维动态规划LeetCode Go 题解精讲1143. Longest Common Subsequence最长公共子序列二维动态规划 导读 本文围绕 leetco示例工程CLRS 15.4 习题精讲最长公共子序列LCS与最长递增子序列LIS的动态规划算法CLRS 15.4 习题精讲最长公共子序列LCS与最长递增子序列LIS的动态规划算法 本文围绕《算法导论》Introduction to Algor文档教程示例工程上一篇Mole一条命令告别几十 GB 的 Mac 磁盘垃圾下一篇30-seconds-of-codeCookies、Local Storage 与 Session Storage 三大前端存储方案对比与实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考