在算法学习中,最长公共子序列(Longest Common Subsequence,简称 LCS) 是动态规划领域最经典的问题之一。它不仅能帮我们理解 DP 思想,还广泛应用于字符串相似度对比、DNA 基因序列匹配、文本对比等实际场景。
本文将基于你写的 C++ 代码,从暴力递归 → 记忆化递归 → 非递归动态规划 → 打印序列,一步步拆解思路、解析代码,带你彻底吃透 LCS!
一、什么是最长公共子序列?
先明确两个核心概念:
- 子序列(Subsequence):不需要连续,但顺序保持不变
- 公共子序列:两个字符串都存在的子序列
- 最长公共子序列:长度最长的那一个
示例:
X = {#, A,B,C,B,D,A,B}Y = {#, B,D,C,A,B,A}- LCS 结果 :
B C B A或B D A B,长度 = 4
分析图解:

二、解法 1:纯暴力递归(会重复计算,效率极低)
这是最直观的递归思路,但存在大量重复子问题,计算量爆炸。
代码实现
cpp
// 最长公共子序列 ------ 暴力递归
int num = 0; // 统计递归调用次数
int LCSLength(const char* X, const char* Y, int i, int j)
{
num += 1;
// 递归出口:任意一个字符串遍历完
if (i == 0 || j == 0)
{
return 0;
}
// 字符相等:LCS 长度 +1,同时向前递归
else if (X[i] == Y[j])
{
return LCSLength(X, Y, i - 1, j - 1) + 1;
}
// 字符不等:取两种情况的最大值(删 X 最后一个 或 删 Y 最后一个)
else
{
int t1 = LCSLength(X, Y, i - 1, j);
int t2 = LCSLength(X, Y, i, j - 1);
return t1 > t2 ? t1 : t2;
}
}
代码解析
- 出口条件 :
i==0或j==0,LCS 长度为 0 - 字符相等 :
X[i] == Y[j]- 说明当前字符属于 LCS
- 结果 = 左上角子问题 + 1
- 字符不等 :
- 结果 = max (去掉 X 当前字符,去掉 Y 当前字符)
- 缺点 :
- 大量重复递归
- 时间复杂度:指数级 O (2ⁿ)
- 稍微长一点的字符串就会卡死
三、解法 2:记忆化递归(带备忘录,避免重复计算)
暴力递归效率太低,我们用二维数组 c 存储已经计算过的结果,避免重复递归。
同时增加 s 数组 ,用来记录路径,方便后面打印 LCS。
核心思路
c[i][j]:存储X[0~i]和Y[0~j]的 LCS 长度s[i][j]:记录路径标记1= 来自左上角(字符相等)2= 来自上方3= 来自左方
代码实现
cpp
int num = 0;
int LCSLength(const char* X, const char* Y, int i, int j,
std::vector<std::vector<int>>& c,
std::vector<std::vector<int>>& s)
{
num += 1;
if (i == 0 || j == 0)
return 0;
// 已经计算过,直接返回(备忘录核心)
if (c[i][j] > 0)
return c[i][j];
if (X[i] == Y[j])
{
c[i][j] = LCSLength(X, Y, i - 1, j - 1, c, s) + 1;
s[i][j] = 1; // 标记:来自左上角
}
else
{
int t1 = LCSLength(X, Y, i - 1, j, c, s);
int t2 = LCSLength(X, Y, i, j - 1, c, s);
if (t1 > t2)
{
c[i][j] = t1;
s[i][j] = 2; // 来自上方
}
else
{
c[i][j] = t2;
s[i][j] = 3; // 来自左方
}
}
return c[i][j];
}
代码解析
- c 数组:缓存结果,避免重复递归
- s 数组:记录每一步的来源方向
- 效率大幅提升:时间复杂度 O (mn)
四、解法 3:非递归动态规划(标准最优解)
这是工业级标准写法 ,用双层循环填充 DP 表,效率最高、最稳定。
核心 DP 公式
cpp
if(X[i] == Y[j])
c[i][j] = c[i-1][j-1] + 1
else
c[i][j] = max(c[i-1][j], c[i][j-1])
代码实现
cpp
int NiceLCSLength(const char* X, const char* Y, int xm, int yn,
std::vector<std::vector<int>>& c, std::vector<std::vector<int>>& s)
{
for (int i = 1; i <= xm; ++i) // 遍历 X
{
for (int j = 1; j <= yn; ++j) // 遍历 Y
{
if (X[i] == Y[j])
{
c[i][j] = c[i - 1][j - 1] + 1;
s[i][j] = 1;
}
else
{
if (c[i - 1][j] > c[i][j - 1])
{
c[i][j] = c[i - 1][j];
s[i][j] = 2;
}
else
{
c[i][j] = c[i][j - 1];
s[i][j] = 3;
}
}
}
Print2Vec(c); // 打印 DP 表
}
return c[xm][yn];
}
代码解析
- 从
i=1,j=1开始逐行逐列填表 - 字符相等 → 左上角 +1
- 不等 → 取 max (上,左)
- 最终答案:c[xm][yn]
五、功能:打印最长公共子序列
根据 s 数组的路径标记,递归回溯输出 LCS。
cpp
void LCS(const char* X, int i, int j, const std::vector<std::vector<int>>& s)
{
if (i == 0 || j == 0)
return;
// 1 = 来自左上角,字符属于 LCS
if (s[i][j] == 1)
{
LCS(X, i - 1, j - 1, s);
printf("%5c", X[i]);
}
// 2 = 来自上方
else if (s[i][j] == 2)
{
LCS(X, i - 1, j, s);
}
// 3 = 来自左方
else
{
LCS(X, i, j - 1, s);
}
}
六、测试主函数 + 打印 DP 表
cpp
void Print2Vec(const std::vector<std::vector<int>>& c)
{
int yn = c[0].size();
int xm = c.size();
printf(" ");
for (int i = 0; i < yn; ++i)
printf("%5d", i);
printf("\n");
for (int i = 0; i < xm; ++i)
{
printf("%3d ", i);
for (int j = 0; j < yn; ++j)
printf("%5d", c[i][j]);
printf("\n");
}
printf("\n---------------------------\n");
}
int main()
{
const int xm = 7;
const int yn = 6;
char X[xm + 1] = { '#','A','B','C','B','D','A','B' };
char Y[yn + 1] = { '#','B','D','C','A','B','A' };
// 创建 DP 表和路径表
std::vector<std::vector<int>> c(xm + 1, std::vector<int>(yn + 1, 0));
std::vector<std::vector<int>> s(xm + 1, std::vector<int>(yn + 1, 0));
// 非递归 DP(最优)
int maxlen = NiceLCSLength(X, Y, xm, yn, c, s);
cout << "最长公共子序列长度:" << maxlen << endl;
// 打印序列
printf("最长公共子序列:");
LCS(X, xm, yn, s);
return 0;
}
运行结果
bash
最长公共子序列长度:4
最长公共子序列: B C B A
七、三种方法对比总结
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 暴力递归 | O(2ⁿ) | O(m+n) | 思路简单,效率极低 |
| 记忆化递归 | O(mn) | O(mn) | 避免重复计算 |
| 非递归 DP | O(mn) | O(mn) | 最优解,推荐使用 |
八、文章总结
- LCS 是动态规划经典模板题,必须掌握
- 核心思想:
- 相等 → 左上角 +1
- 不等 → max (上,左)
- c 数组存长度,s 数组存路径
- 标准最优解:非递归 DP,时间 O (mn),空间 O (mn)
- 可用于:字符串匹配、基因比对、diff 工具
本文代码均可直接编译运行,非常适合 DP 初学者学习~