最长公共子序列 图解题解
这道题到底在问什么
- 输入
- text1 = "ABCD" text2 = "ACBAD"
- 输出
- 3(如 "ABD" 或 "ACD")
最优解:为什么这么做
一句话答案:LeetCode 1143 最长公共子序列:开一张二维 dp 表,dp[i][j] 记两串前缀的公共子序列长度,当前字符相等就接左上角 +1、不等就取上、左较大值,右下角即答案。时间 O(m·n)、空间 O(m·n)。
最长公共子序列,为什么可以跳着挑字符
给两个字符串 text1、text2,各删掉若干字符后,找最长的公共部分。子序列是跳着挑的字符:剩下的按原顺序排好就行,不必挨着,这和「必须连续截一段」的子串不同。题面 text1 = "ABCD"、text2 = "ACBAD",最长公共子序列长 3,比如 "ABD"。
把所有子序列都列出来,为什么算不完
把 text1 每个子序列都拼出来到 text2 里核对,长度 m 的串有 2^m 个子序列(每字符选或不选),串一长指数爆炸枚举不完。既然前缀比对被反复重验,就把「text1 前 i 个、text2 前 j 个字符的最长公共子序列多长」算一次存下来复用。
一张二维表,dp[i][j] 到底存什么
用动态规划(把「text1 前 i、text2 前 j 个字符的答案」存进表、后面直接取不重算):dp[i][j] 就是 text1 前 i、text2 前 j 个字符的最长公共子序列长度,(i,j) 是(行号,列号)从 0 数起,一格即一个状态(推进到哪一步、手里那个答案)。表比两串各多开一圈,第 0 行、第 0 列填 0(任一串取 0 个字符时公共部分就是 0);这圈 0 是地基,靠它每格都能直接看左上、上、左三个邻格。
相等接左上、不等取较大,凭什么是对的
表多开了一圈,dp[i][j] 里的当前两字符其实是 text1[i-1]、text2[j-1]。它俩相等时,这个公共字符能接到「两边都还没用它」那段答案后加一,而那正是斜上方 dp[i-1][j-1],故 dp[i][j] = dp[i-1][j-1] + 1。不等时两字符不能同用,就在正上 dp[i-1][j] 与正左 dp[i][j-1] 里取较大,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。这就是递推(拿邻格推出当前格)。
拿 ABCD 和 ACBAD 逐格把表填出来
text1 = "ABCD" 作行、text2 = "ACBAD" 作列,第 0 行、第 0 列铺 0。第 1 行填 A:和 text2 的两个 A 相等、都接左上 0 得 1,其余取较大也是 1,整行 1 1 1 1 1。
第 2 行 B:第三列相等 dp[2][3] 接左上 dp[1][2]=1 得 2,整行 1 1 2 2 2。第 3 行 C 同理,整行 1 2 2 2 2。第 4 行 D:末列相等 dp[4][5] 接左上 dp[3][4]=2 得 3,整行 1 2 2 2 3。右下角 3 就是答案。
dp 下标错记成 text1[i],整张表为什么会全错位
表满 m×n 格、每格常数时间,时间 O(m·n)(大 O 记号,描述串变长时计算量怎么涨);空间也是一张表 O(m·n),只求长度时每格只依赖上一行和本行左边,留两行滚着填(滚动数组:留最近几行覆盖旧值)压到 O(min(m,n))。最坑的是下标错位:dp[i][j] 对的是 text1[i-1]、text2[j-1],写成 text1[i]、text2[j] 就整体偏一格、整张表全错。边界都被那圈 0 兜住:两串全同取到全长,空串或毫无公共都取 0。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条「字符相等取左上角+1,不等取上、左较大值」,下面每一格都在套它。
- 4dp[0][*]=dp[*][0]=0空串行列都是 0:text1 或 text2 取 0 个字符时,公共子序列长度当然是 0。这是递推的地基,接下来从左上往右下、一行一行填内部的格子。
- 5dp[1][1] = dp[0][0] + 1 = 1当前格 dp[1][1]:text1 的 A 和 text2 的 A 相等!可以在左上角 dp[0][0]=0 的基础上接长一位 → 填 1。
- 6dp[1][2] = max(上 0, 左 1) = 1当前格 dp[1][2]:text1 的 A 和 text2 的 C 不等,这两个字符不能同时用。取上面 0 和左边 1 里较大的 → 填 1。
- 7dp[1][3] = max(上 0, 左 1) = 1当前格 dp[1][3]:text1 的 A 和 text2 的 B 不等,这两个字符不能同时用。取上面 0 和左边 1 里较大的 → 填 1。
- 8dp[1][4] = dp[0][3] + 1 = 1当前格 dp[1][4]:text1 的 A 和 text2 的 A 相等!可以在左上角 dp[0][3]=0 的基础上接长一位 → 填 1。
- 9dp[1][5] = max(上 0, 左 1) = 1当前格 dp[1][5]:text1 的 A 和 text2 的 D 不等,这两个字符不能同时用。取上面 0 和左边 1 里较大的 → 填 1。
- 10dp[2][1] = max(上 1, 左 0) = 1当前格 dp[2][1]:text1 的 B 和 text2 的 A 不等,这两个字符不能同时用。取上面 1 和左边 0 里较大的 → 填 1。
- 11dp[2][2] = max(上 1, 左 1) = 1当前格 dp[2][2]:text1 的 B 和 text2 的 C 不等,这两个字符不能同时用。取上面 1 和左边 1 里较大的 → 填 1。
- 12dp[2][3] = dp[1][2] + 1 = 2当前格 dp[2][3]:text1 的 B 和 text2 的 B 相等!可以在左上角 dp[1][2]=1 的基础上接长一位 → 填 2。
- 13dp[2][4] = max(上 1, 左 2) = 2当前格 dp[2][4]:text1 的 B 和 text2 的 A 不等,这两个字符不能同时用。取上面 1 和左边 2 里较大的 → 填 2。
- 14dp[2][5] = max(上 1, 左 2) = 2当前格 dp[2][5]:text1 的 B 和 text2 的 D 不等,这两个字符不能同时用。取上面 1 和左边 2 里较大的 → 填 2。
- 15dp[3][1] = max(上 1, 左 0) = 1当前格 dp[3][1]:text1 的 C 和 text2 的 A 不等,这两个字符不能同时用。取上面 1 和左边 0 里较大的 → 填 1。
- 16dp[3][2] = dp[2][1] + 1 = 2当前格 dp[3][2]:text1 的 C 和 text2 的 C 相等!可以在左上角 dp[2][1]=1 的基础上接长一位 → 填 2。
- 17dp[3][3] = max(上 2, 左 2) = 2当前格 dp[3][3]:text1 的 C 和 text2 的 B 不等,这两个字符不能同时用。取上面 2 和左边 2 里较大的 → 填 2。
- 18dp[3][4] = max(上 2, 左 2) = 2当前格 dp[3][4]:text1 的 C 和 text2 的 A 不等,这两个字符不能同时用。取上面 2 和左边 2 里较大的 → 填 2。
- 19dp[3][5] = max(上 2, 左 2) = 2当前格 dp[3][5]:text1 的 C 和 text2 的 D 不等,这两个字符不能同时用。取上面 2 和左边 2 里较大的 → 填 2。
- 20dp[4][1] = max(上 1, 左 0) = 1当前格 dp[4][1]:text1 的 D 和 text2 的 A 不等,这两个字符不能同时用。取上面 1 和左边 0 里较大的 → 填 1。
- 21dp[4][2] = max(上 2, 左 1) = 2当前格 dp[4][2]:text1 的 D 和 text2 的 C 不等,这两个字符不能同时用。取上面 2 和左边 1 里较大的 → 填 2。
- 22dp[4][3] = max(上 2, 左 2) = 2当前格 dp[4][3]:text1 的 D 和 text2 的 B 不等,这两个字符不能同时用。取上面 2 和左边 2 里较大的 → 填 2。
- 23dp[4][4] = max(上 2, 左 2) = 2当前格 dp[4][4]:text1 的 D 和 text2 的 A 不等,这两个字符不能同时用。取上面 2 和左边 2 里较大的 → 填 2。
- 24dp[4][5] = dp[3][4] + 1 = 3当前格 dp[4][5]:text1 的 D 和 text2 的 D 相等!可以在左上角 dp[3][4]=2 的基础上接长一位 → 填 3。
- 25LCS 长度 = 3整张表填完!最右下角 dp[4][5] = 3 就是 text1 全部和 text2 全部的最长公共子序列长度。从这格沿着「相等就走左上、不等就走较大邻格」回溯,还能还原出具体的公共子序列。
⚠️ 容易写错的地方
✗ 错:下标对不齐(用 text1[i] 配 dp[i])
✓ 对:dp[i][j] 对应 text1[i-1]、text2[j-1]
dp 多开了一圈 0,行列下标比字符串下标大 1,错位会全表算错
✗ 错:相等时取 max(上,左)
✓ 对:相等时必须走左上角 +1
相等却不接上左上角,会漏算这一对匹配,结果偏小
✗ 错:把子序列当成子串(要连续)
✓ 对:子序列可不连续
子串要连续是另一类题(LC718),递推式不同,别混
完整代码(Python / Java / C++)
Python
def longestCommonSubsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)] # 多一圈 0
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # 相等:左上+1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # 不等:取大
return dp[m][n] # 右下角即答案Java
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1))
dp[i][j] = dp[i-1][j-1] + 1; // 相等:左上+1
else
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 取大
}
}
return dp[m][n];
}C++
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1[i-1] == text2[j-1])
dp[i][j] = dp[i-1][j-1] + 1; // 相等:左上+1
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]); // 取大
}
}
return dp[m][n];
}复杂度
时间
O(m·n)
表有 m×n 个格子,每格 O(1) 推出
空间
O(m·n)
存整张表;可滚动数组优化到 O(min(m,n))
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长公共子序列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
子序列和子串到底差在哪,这题为什么算的是子序列?+
子串要求在原串里连续截一段,子序列只要求保持原来的先后顺序、可以隔着挑字符。本题 text1 = "ABCD" 和 text2 = "ACBAD" 的公共子序列 "ABD" 里,B 和 D 在 text1 里并不相邻,却仍算数,因为只需按序出现、不需挨着。要是改成求最长公共子串(连续),转移就变了:字符相等才接左上加一,一旦不等直接归 0,答案取所有格子里的最大值而不是右下角。
只想要长度,能不能把二维表压成一维省空间?+
能。dp[i][j] 只用到上一行和本行已填好的左边,所以留两行来回轮换(滚动数组)就够,空间从 O(m·n) 降到 O(min(m,n)),做法是把较短的串当列、行数只保留两行。代价是丢掉了整张表,没法再回溯还原出具体是哪几个字符组成的公共子序列;要还原就得留着完整的二维表,从右下角沿「相等走左上、不等走较大邻格」倒着走回去。
这套填表能直接迁移到哪些题?+
只要把「两个序列求最长公共部分」的壳换个说法,转移一模一样。LeetCode 1035 不相交的线,把两排数字连线、不许交叉、求最多能连几条,等价于求这两排数字的最长公共子序列。LeetCode 1092 两个字符串的最短公共超序列,先用同一张 dp 表求出最长公共子序列,再沿表回溯,把两串里没被公共部分覆盖的字符补进去。会了这张表,这几道只是换层皮。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长公共子序列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。