最长公共子序列问题 & 最长公共子串问题

最长公共子序列问题#

题目:最长公共子序列

《程序员代码面试指南》第65题 P220 难度:尉★★☆☆

本题是非常经典的动态规划问题,先来介绍求解动态规划表的过程。如果str1长度Mstr2长度N,生成大小为M×N的矩阵dpdp[i][j]的含义是str1[0..i]与str2[0..j]的最长公共子序列的长度从左到右再从上到下计算矩阵dp

  1. 矩阵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
  2. 矩阵dp第一行dp[0][0..N-1]与步骤1同理
  3. 对于其他位置(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矩阵的状态,可以得到最长公共子序列。具体方法如下:

  1. 从矩阵的右下角开始,有三种移动方式:向上向左向左上。假设移动的过程中,i表示此时的行数,j表示此时的列数,同时用一个变量res来表示最长公共子序列。
  2. 如果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,然后向左上方移动
  3. 如果dp[i][j]等于dp[i-1][j],说明之前在计算dp[i][j]的时候,dp[i-1][j-1]+1这个决策不是必须选择的决策,向上方移动即可。
  4. 如果dp[i][j]等于dp[i][j-1],与步骤3同理,向左方移动
  5. 如果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大小的矩阵dpdp[i][j]表示在必须把str1[i]和str2[j]当作公共子串最后一个字符的情况下公共子串最长能有多长。具体过程如下:

  1. 矩阵dp第一列dp[0..M-1][0],如果str1[i]==str2[0]dp[i][0]=1否则dp[i][0]=0
  2. 矩阵dp第一列dp[0][0..N-1],如果str1[0]==str2[j]dp[0][j]=1否则dp[0][j]=0
  3. 其它位置按照从左到右再从上到下来计算。如果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);
	}
posted @   幻梦翱翔  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
主题色彩