最短公共超序列 图解题解
这道题到底在问什么
- 输入
- str1 = "abac", str2 = "cab"
- 输出
- "cabac"(长度 5,两串都能从中跳着取出来)
- 输入
- str1 = "abc", str2 = "abc"
- 输出
- "abc"(完全相同,不用补任何字符)
- 输入
- str1 = "abc", str2 = "def"
- 输出
- "abcdef"(毫无公共字符,只能拼起来)
最优解:一步一步想明白
- 3一句话套路:最短超序列 = 把 LCS 当骨架,公共字符只写一次、独有字符按原序补入。先填 dp 表求 LCS,再沿表重构。
- 4先搭骨架:行对应 str1「abac」的字符,列对应 str2「cab」的字符,行列末尾各加一格 ∅ 代表「空后缀」。dp[i][j] 表示 str1 第 i 个起的后缀 与 str2 第 j 个起的后缀 的 LCS 长度,现在全是「·」表示还没算。我们从右下往左上填。
- 5最下面一行是 str1 的空后缀:空串和任何串的 LCS 都是 0,整行填 0(紫格)。它是后面所有格往下看时的地基。
- 6最右边一列是 str2 的空后缀:同理 LCS 都是 0,整列填 0(紫格)。基础格备齐,接下来从右下角内部一格格往左上推。
- 7看 "c" 和 "b":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[4][2]=0(跳过 str1 这个字符),右方 dp[3][3]=0(跳过 str2 这个字符),取较大 = 0(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 8看 "c" 和 "a":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[4][1]=0(跳过 str1 这个字符),右方 dp[3][2]=0(跳过 str2 这个字符),取较大 = 0(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 9看 "c"(str1 第 4 个)和 "c"(str2 第 1 个):相同!这个字符能进 LCS,长度 = 右下角 dp[4][1]=0 再加 1 = 1(紫格,蓝格是它依赖的右下角)。
- 10看 "a" 和 "b":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[3][2]=0(跳过 str1 这个字符),右方 dp[2][3]=0(跳过 str2 这个字符),取较大 = 0(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 11看 "a"(str1 第 3 个)和 "a"(str2 第 2 个):相同!这个字符能进 LCS,长度 = 右下角 dp[3][2]=0 再加 1 = 1(紫格,蓝格是它依赖的右下角)。
- 12看 "a" 和 "c":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[3][0]=1(跳过 str1 这个字符),右方 dp[2][1]=1(跳过 str2 这个字符),取较大 = 1(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 13看 "b"(str1 第 2 个)和 "b"(str2 第 3 个):相同!这个字符能进 LCS,长度 = 右下角 dp[2][3]=0 再加 1 = 1(紫格,蓝格是它依赖的右下角)。
- 14看 "b" 和 "a":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[2][1]=1(跳过 str1 这个字符),右方 dp[1][2]=1(跳过 str2 这个字符),取较大 = 1(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 15看 "b" 和 "c":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[2][0]=1(跳过 str1 这个字符),右方 dp[1][1]=1(跳过 str2 这个字符),取较大 = 1(下方与右方一样长,任选一边都对,这里固定优先取下方,蓝格是两个依赖)。
- 16看 "a" 和 "b":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[1][2]=1(跳过 str1 这个字符),右方 dp[0][3]=0(跳过 str2 这个字符),取较大 = 1(下方更长,蓝格是两个依赖)。
- 17看 "a"(str1 第 1 个)和 "a"(str2 第 2 个):相同!这个字符能进 LCS,长度 = 右下角 dp[1][2]=1 再加 1 = 2(紫格,蓝格是它依赖的右下角)。
- 18看 "a" 和 "c":不同,当前这对没法同时进 LCS,只能跳过其中一个,看哪边后续 LCS 更长。下方 dp[1][0]=1(跳过 str1 这个字符),右方 dp[0][1]=2(跳过 str2 这个字符),取较大 = 2(右方更长,蓝格是两个依赖)。
- 19整张表填满。左上角 dp[0][0] = 2,意思是 "abac" 与 "cab" 的最长公共子序列长度为 2(正是 "ab")。LCS 这副骨架有了,下面沿着 dp 表从左上角走一遍,把答案重构出来。
- 20重构规则:站在格子 (i, j),① 若 str1[i]=str2[j],这是公共字符,只写一次,i、j 同时右下移;② 若不同,跟着「LCS 更长」的那一边走,把被跳过那一侧的独有字符先写进答案。指针从 (0,0) 出发。
- 21当前 (0,0):"a" 与 "c" 不同。右方 LCS 2 更大,跟着右方走,要跳过 str2 的 "c",把这个 str2 独有字符先补进答案,只 j++(蓝格往右)。答案现在是 "c"。
- 22当前 (0,1):"a" 两边相同,是公共字符,只写一次 "a" 进答案,然后 i、j 一起往右下走(绿格表示已落定)。答案现在是 "ca"。
- 23当前 (1,2):"b" 两边相同,是公共字符,只写一次 "b" 进答案,然后 i、j 一起往右下走(绿格表示已落定)。答案现在是 "cab"。
- 24指针有一边走到了头(到了 ∅ 边界),剩下另一边的字符不会再有公共部分,原样全部接上即可。这里 str1 还剩 "ac"、str2 还剩 "空",接上后答案 = "cabac"。
- 25复盘:LCS "ab" 是公共骨架只写一次,str2 独有的 "c" 在前面补、str1 独有的 "ac" 在后面补,拼成 "cabac"。验证一下:从它里能跳取出 "abac"(原 str1)和 "cab"(原 str2),且长度 = str1 长 4 + str2 长 3 − LCS 长 2 = 5,无法更短。
⚠️ 容易写错的地方
✗ 错:公共字符写了两次
✓ 对:相同字符重构时只写一次,i、j 同时前进
写两次会让超序列变长、不再最短
✗ 错:不同字符时随便挑一边走
✓ 对:比较 dp[i+1][j] 与 dp[i][j+1],跟更长的 LCS 走
挑错方向会破坏后续公共子序列,结果偏长
✗ 错:忘了接尾巴或丢了 dp 表
✓ 对:结束后把两侧剩余字符接上,且 dp 表全程保留供重构查询
漏接尾巴会缺字符;只存长度还原不出具体答案
完整代码(Python / C++ / Java)
Python
class Solution:
def shortestCommonSupersequence(self, str1: str, str2: str) -> str:
m, n = len(str1), len(str2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
if str1[i] == str2[j]:
dp[i][j] = dp[i + 1][j + 1] + 1
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j + 1])
i = j = 0
ans = []
while i < m and j < n:
if str1[i] == str2[j]:
ans.append(str1[i]); i += 1; j += 1
elif dp[i + 1][j] >= dp[i][j + 1]:
ans.append(str1[i]); i += 1
else:
ans.append(str2[j]); j += 1
ans.append(str1[i:]); ans.append(str2[j:])
return ''.join(ans)C++
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string shortestCommonSupersequence(string str1, string str2) {
int m = str1.size(), n = str2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = m - 1; i >= 0; --i) for (int j = n - 1; j >= 0; --j)
dp[i][j] = str1[i] == str2[j] ? dp[i+1][j+1] + 1 : max(dp[i+1][j], dp[i][j+1]);
int i = 0, j = 0;
string ans;
while (i < m && j < n) {
if (str1[i] == str2[j]) { ans.push_back(str1[i]); i++; j++; }
else if (dp[i+1][j] >= dp[i][j+1]) ans.push_back(str1[i++]);
else ans.push_back(str2[j++]);
}
ans += str1.substr(i) + str2.substr(j);
return ans;
}
};Java
import java.util.*;
class Solution {
public String shortestCommonSupersequence(String str1, String str2) {
int m = str1.length(), n = str2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = m - 1; i >= 0; i--) for (int j = n - 1; j >= 0; j--)
dp[i][j] = str1.charAt(i) == str2.charAt(j) ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
int i = 0, j = 0;
StringBuilder ans = new StringBuilder();
while (i < m && j < n) {
if (str1.charAt(i) == str2.charAt(j)) { ans.append(str1.charAt(i)); i++; j++; }
else if (dp[i + 1][j] >= dp[i][j + 1]) ans.append(str1.charAt(i++));
else ans.append(str2.charAt(j++));
}
ans.append(str1.substring(i)).append(str2.substring(j));
return ans.toString();
}
}复杂度
时间
O(mn)
m、n 为两串长度。填 dp 表 m×n 个格子各 O(1);重构最多走 m+n 步
空间
O(mn)
需要完整的 dp 二维表参与重构,无法只用滚动数组(重构要回看任意格)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最短公共超序列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么必须保留完整 dp 表,不能像普通 LCS 那样用滚动数组省空间?+
普通 LCS 只要长度,一行滚动就够。但本题要还原出具体字符串,重构阶段会从 (0,0) 一路往右下走,每一步都要比较 dp[i+1][j] 与 dp[i][j+1] 来决定方向,这需要随机访问表里任意位置的值。滚动数组只留一行,过去的状态被覆盖了就回不去,所以必须保留 O(mn) 的完整表。
当 dp[i+1][j] 等于 dp[i][j+1] 时往哪边走,会影响答案吗?+
不影响长度,可能影响具体字符串。两边 LCS 相等说明往任一方向都能得到同样最短的超序列,只是字符排列可能不同。题目通常允许返回任意一个最短解,所以代码里用 dp[i+1][j] ≥ dp[i][j+1] 固定优先走 str1 一侧,保证结果确定即可。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 最短公共超序列 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。