• 【算法面试】常见动态规划算法示例1-最长公共子串问题


    【题 目 】
    给定两个字符串str1和 str2,返回两个字符串的最长公共子串。
    【举 例 】
    str1=”1AB2345CD”, str2=”12345EF”,返回”2345″。
    【要 求 】
    如 果 str1长 度 为 M , str2长 度 为 N , 实现时间复杂度为 O ( M x N ),额外空间复杂度为
    0(1)的方法。
    【难 度 】
    ★ ★ ★ ☆

    解答


    经典动态规划的方法可以做到时间复杂度为 O ( M*N ),额外空间复杂度O( M*N ),经过优化之后的实现可以把额外空间复杂度从O( M*N )降至 0 (1 ),我们先来介绍经典方法。
    首先需要生成动态规划表。生成大小为 M*N 的矩阵dp ,行数为M, 列数为 N 。 dp[i][j]的含义是,在必须把str1[i]和 str2[j]当作公共子串最后一个字符的情况下,公共子串最长能有多长。比如,str1=”A1234B”, str2=”CD1234″, dp[3][4]的含义是在必须把 str1[3]当作公共子串最后一个字符的情况下,公共子串最长能有多长。这种情况下的最长公共子串为”1 2 3 “,所 以 dp[3][4]为 3。再如, str1=”A12E4B”, str2=”CD12F4″,dp[3][4]的含义是在必须把str1[3]( 即’E ‘) 和 str2[4]( 即’F’)当作公共子串最后一个字符的情况下,公共子串最长能有多长。这种情况下根本不能构成公共子串,所 以 dp[3][4]为 0。


    介绍了 dp[i][j]的意义后,接下来介绍dp[i][j]怎么求。具体过程如下:

    1. 矩 阵 dp第一列即dp[0..M-1][0]。对某一个位置(i,0)来说,如果 str1[i]=str2[0] , 令dp[i][0]=1, 否则令 dp[i][0]=0。比如 str1=”ABAC”, ,str2[0]=”A”。dp 矩阵第一列上的值依次为 dp[0][0]=1, dp[1][0]=0, dp[2][0]=1, dp[3][0]=0
    2. 矩 阵 d p 第 一 行 即 dp[0][0..N-1]与 步 骤 1 同理。对某一个位置(0,j)来 说 ,如果str1[0]== str2[j] . 令 dp[0][j]=1, 否则令 dp[0][j]=0。
    3.  其他位置按照从左到右,再从上到下来计算,dp[i][j]的值只可能有两种情况。
      • 如 果 str1[i]!=str2[j],说明在必须把str1[i]和 str2[j] 当作公共子串最后一个字符是不 可能的, 令 dp[i][j]=0。
      • 如 果 str1[i]==str2[j],说 明 str1[i]和 str2[j]可以作为公共子串的最后一个字符,从最 后 一 个 字 符 向 左 能 扩 多 大 的 长 度 呢 ? 就 是 dp [i-1][j-1]的 值 , 所 以 令dp[i][j]=dp[i-1][j-1]+1
      如果 str1=”abcde”,str2=”bebcd”, 计算的 dp 矩阵如下:

    
    	public static 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中,最 大 值 是 dp[3][4]==3,说明最长公共子串的长度为3。最长公共子串的最后一个字符是str1[3],当然也是str2[4],因为两个字符一样。那么最长公共子串为从strl[3]开始向左一共3 字节的子串,即 strl[1..3],当然也是str2[2..4]。总之,遍 历 dp找到最大值及其位置,最长公共子串自然可以得到。具体过程请参看如下代码中的Icstl方法,也是整个过程的主方法。

    public static 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);
    	}

    经典动态规划的方法需要大小为MxN的 dp矩阵,但实际上是可以减小至o(1)的,因为我们注意到计算每一个dp[i][j]的时候,最多只需要其左上方d p[i-l][j-l]的值,所以按照斜线方向来计算所有的值,只需要一个变量就可以计算出所有位置的值,如 图 所示

    每一条斜线在计算之前生成整型变量len, len表示左上方位置的值,初 始 时 len=0。从斜线最左上的位置幵始向右下方依次计算每个位置的值,假设计算到位置(i,j),此 时 len表示 位 置 的 值 。如果 strl [i]=str2[j],那么位置( i-1,j-1 )的值为 len+l,如果 strl[i]!=str2[j] ,那么位置(i,j)的值为0。计算后将len更新成位置(i,j)的值,然后计算下一个位置,即(i+1,j+1) 位置的值。依次计算下去就可以得到斜线上每个位置的值,然后算下一条斜线。用全局变量 max记录所有位置的值中的最大值3 最大值出现时,用全局变量end记录其位置即可。
    具体过程请参看如下代码中的lcst2方法

    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);
    	}
  • 相关阅读:
    Android中Activity之间通信
    vs2017 2019 下载更新慢的解决方法
    c# 判断某个类是否实现某个接口
    mvc api 关于 post 跟get 请求的一些想法[FromUri] 跟[FromBody] 同一个控制器如何实现共存
    vs2017 mvc 自定义路由规则 出现 404.0 错误代码 0x80070002
    C# winform 发布的时候没有app.config去哪儿了?
    安装c#服务
    Type.GetType反射的对象创建Activator.CreateInstance
    c# 谷歌动态口令对接
    asp.net mvc 异步控制器
  • 原文地址:https://www.cnblogs.com/jobbible/p/10614648.html
Copyright © 2020-2023  润新知