最长公共子序列问题 & 最长公共子串问题
最长公共子序列问题#
题目:最长公共子序列
《程序员代码面试指南》第65题 P220 难度:尉★★☆☆
本题是非常经典的动态规划问题,先来介绍求解动态规划表的过程。如果str1的长度为M,str2的长度为N,生成大小为M×N的矩阵dp。dp[i][j]的含义是str1[0..i]与str2[0..j]的最长公共子序列的长度。从左到右,再从上到下计算矩阵dp。
- 矩阵dp第一列即dp[0..M-1][0],dp[i][0]的含义是str1[0..i]与str2[0]的最长公共子序列长度。显然dp[i][0]最大为1。一旦dp[i][0]被设置为1,之后的dp[i+1..M-1][0]也都为1。
- 矩阵dp第一行即dp[0][0..N-1]与步骤1同理。
- 对于其他位置(i,j),dp[i][j]的值可能来自这三种情况:①可能是dp[i-1][j] ②可能是dp[i][j-1] ③如果str1[i]==str2[j],还可能是dp[i-1][j-1]+1。这三个可能的值中,选最大的作为dp[i][j]的值。
具体过程参看如下的getdp方法:
public int[][] getdp(char[] str1, char[] str2) {
int[][] dp = new int[str1.length][str2.length];
dp[0][0] = str1[0] == str2[0] ? 1 : 0;
for (int i = 1; i < str1.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
}
for (int j = 1; j < str2.length; j++) {
dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
}
for (int i = 1; i < str1.length; i++) {
for (int j = 1; j < str2.length; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (str1[i] == str2[j]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
return dp;
}
dp矩阵中最右下角的值代表str1整体和str2整体的最长公共子序列的长度。通过整个dp矩阵的状态,可以得到最长公共子序列。具体方法如下:
- 从矩阵的右下角开始,有三种移动方式:向上、向左、向左上。假设移动的过程中,i表示此时的行数,j表示此时的列数,同时用一个变量res来表示最长公共子序列。
- 如果dp[i][j]大于dp[i-1][j]和dp[i][j-1],说明之前在计算dp[i][j]的时候,一定是选择了决策dp[i-1][j-1]+1,可以确定str1[i]等于str2[j],并且这个字符一定属于最长公共子序列,把这个字符放进res,然后向左上方移动。
- 如果dp[i][j]等于dp[i-1][j],说明之前在计算dp[i][j]的时候,dp[i-1][j-1]+1这个决策不是必须选择的决策,向上方移动即可。
- 如果dp[i][j]等于dp[i][j-1],与步骤3同理,向左方移动。
- 如果dp[i][j]同时等于dp[i-1][j]和dp[i][j-1],向上还是向下无所谓,选择其中一个即可,反正不会错过必须选择的字符。
也就是说,通过dp求解最长公共子序列的过程就是还原出当时如何求解dp的过程,来自哪个策略,就朝哪个方向移动。全部过程请参看如下代码中的Icse方法。
public String lcse(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
return "";
}
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int[][] dp = getdp(chs1, chs2);
int m = chs1.length - 1;
int n = chs2.length - 1;
char[] res = new char[dp[m][n]];
int index = res.length - 1;
while (index >= 0) {
if (n > 0 && dp[m][n] == dp[m][n - 1]) {
n--;
} else if (m > 0 && dp[m][n] == dp[m - 1][n]) {
m--;
} else {
res[index--] = chs1[m];
m--;
n--;
}
}
return String.valueOf(res);
}
总的时间复杂度为O(M×N)。如果本题只想求最长公共子序列的长度,则可以使用空间压缩的方法。
----------------------------------------------------------------------------------------------------------------------------
(2022年3月20日11:49:03 更新)
最长公共子串问题#
题目:最长公共子串
《程序员代码面试指南》第66题 P223 难度:校★★★☆
本题要求时间复杂度为O(M×N),额外空间复杂度为O(1)。
首先介绍经典动态规划方法,可以做到时间复杂度为O(M×N),额外空间复杂度为O(M×N)。
首先生成动态规划表,即M×N大小的矩阵dp。dp[i][j]表示在必须把str1[i]和str2[j]当作公共子串最后一个字符的情况下,公共子串最长能有多长。具体过程如下:
- 矩阵dp第一列dp[0..M-1][0],如果str1[i]==str2[0],dp[i][0]=1,否则dp[i][0]=0。
- 矩阵dp第一列dp[0][0..N-1],如果str1[0]==str2[j],dp[0][j]=1,否则dp[0][j]=0。
- 其它位置按照从左到右,再从上到下来计算。如果str1[i]!=str2[j],dp[i][j]=0;如果str1[i]==str2[j],dp[i][j]=dp[i-1][j-1]+1。
计算dp矩阵的具体过程如下:
public int[][] getdp(char[] str1, char[] str2) {
int[][] dp = new int[str1.length][str2.length];
for (int i = 0; i < str1.length; i++) {
if (str1[i] == str2[0]) {
dp[i][0] = 1;
}
}
for (int j = 1; j < str2.length; j++) {
if (str1[0] == str2[j]) {
dp[0][j] = 1;
}
}
for (int i = 1; i < str1.length; i++) {
for (int j = 1; j < str2.length; j++) {
if (str1[i] == str2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
}
}
return dp;
}
生成动态规划表dp后,得到最长公共子串是非常容易的。遍历dp找到最大值及其位置,最长公共子串自然可以得到。具体过程如下,这也是整个过程的主方法:
public String lcst1(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
return "";
}
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int[][] dp = getdp(chs1, chs2);
int end = 0;
int max = 0;
for (int i = 0; i < chs1.length; i++) {
for (int j = 0; j < chs2.length; j++) {
if (dp[i][j] > max) {
end = i;
max = dp[i][j];
}
}
}
return str1.substring(end - max + 1, end + 1);
}
上述过程可以进行优化,使额外空间复杂度减小至O(1)。
因为计算每一个dp[i][j]的时候,最多只需要其左上方dp[i-1][j-1]的值,所以按照斜线方向来计算所有的值,只需要一个变量就可以计算出所有位置的值。
每一条斜线在计算之前生成整型变量len,代表左上方位置的值,初始时为0。从斜线左上的位置开始向右下方依次计算每个位置的值。如果str1[i]==str2[j],那么位置(i,j)的值为len+1,否则为0。然后计算下一个位置。再然后计算下一条斜线。用全局变量max记录所有位置的值中的最大值,最大值出现时,用全局变量end记录其位置即可。具体过程如下:
public static String lcst2(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
return "";
}
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int row = 0; // 斜线开始位置的行
int col = chs2.length - 1; // 斜线开始位置的列
int max = 0; // 记录最大长度
int end = 0; // 最大长度更新时,记录子串的结尾位置
while (row < chs1.length) {
int i = row;
int j = col;
int len = 0;
// 从(i,j)开始向右下方遍历
while (i < chs1.length && j < chs2.length) {
if (chs1[i] != chs2[j]) {
len = 0;
} else {
len++;
}
// 记录最大值,以及结束字符的位置
if (len > max) {
end = i;
max = len;
}
i++;
j++;
}
if (col > 0) { // 斜线开始位置的列先向左移动
col--;
} else { // 列移动到最左之后,行向下移动
row++;
}
}
return str1.substring(end - max + 1, end + 1);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!