动态规划 最长公共子序列 过程图解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划 最长公共子序列 过程图解相关的知识,希望对你有一定的参考价值。

参考技术A

首先需要科普一下,最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。

这里给出一个例子:有两个母串
cnblogs
belong
比如序列bo, bg, lg在母串cnblogs与belong中都出现过并且出现顺序与母串保持一致,我们将其称为公共子序列。最长公共子序列(Longest Common Subsequence,LCS),顾名思义,是指在所有的子序列中最长的那一个。

子串是要求更严格的一种子序列, 要求在母串中连续地出现
在上述例子的中,最长公共子序列为blog(cnblogs,belong),最长公共子串为lo(cnblogs, belong)。

给一个图再解释一下:

如上图,给定的字符序列: a,b,c,d,e,f,g,h,它的子序列示例: a,c,e,f 即元素b,d,g,h被去掉后,保持原有的元素序列所得到的结果就是子序列。同理,a,h,c,d,e等都是它的子序列。
它的子串示例:c,d,e,f 即连续元素c,d,e,f组成的串是给定序列的子串。同理,a,b,c,d,g,h等都是它的子串。

这个问题说明白后,最长公共子序列(以下都简称LCS)就很好理解了。
给定序列s1=1,3,4,5,6,7,7,8,s2=3,5,7,4,8,6,7,8,2,s1和s2的相同子序列,且该子序列的长度最长,即是LCS。
s1和s2的其中一个最长公共子序列是 3,4,6,7,8

求解LCS问题,不能使用暴力搜索方法。 一个长度为n的序列拥有 2的n次方个子序列,它的时间复杂度是指数阶 ,太恐怖了。解决LCS问题,需要借助动态规划的思想。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。 为了避免大量的重复计算,节省时间,我们引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法。

解决LCS问题,需要把原问题分解成若干个子问题,所以需要刻画LCS的特征。

设A=“a0,a1,…,am”,B=“b0,b1,…,bn”,且Z=“z0,z1,…,zk”为它们的最长公共子序列。不难证明有以下性质:
如果am=bn,则zk=am=bn,且“z0,z1,…,z(k-1)”是“a0,a1,…,a(m-1)”和“b0,b1,…,b(n-1)”的一个最长公共子序列;
如果am!=bn,则若zk!=am,蕴涵“z0,z1,…,zk”是“a0,a1,…,a(m-1)”和“b0,b1,…,bn”的一个最长公共子序列;
如果am!=bn,则若zk!=bn,蕴涵“z0,z1,…,zk”是“a0,a1,…,am”和“b0,b1,…,b(n-1)”的一个最长公共子序列。

有些同学,一看性质就容易晕菜,所以我给出一个图来让这些同学理解一下:

以我在第1小节举的例子(S1=1,3,4,5,6,7,7,8和S2=3,5,7,4,8,6,7,8,2),并结合上图来说:

假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 S1减去最后一个元素 与 S2减去最后一个元素 的 LCS 再加上 S1和S2相等的最后一个元素。

假如S1的最后一个元素 与 S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于 : S1减去最后一个元素 与 S2 的LCS, S2减去最后一个元素 与 S1 的LCS 中的最大的那个序列。

假设Z=<z1,z2,⋯,zk>是X与Y的LCS, 我们观察到
如果Xm=Yn,则Zk=Xm=Yn,有Zk−1是Xm−1与Yn−1的LCS;
如果Xm≠Yn,则Zk是Xm与Yn−1的LCS,或者是Xm−1与Yn的LCS。

因此,求解LCS的问题则变成递归求解的两个子问题。但是,上述的递归求解的办法中, 重复的子问题多,效率低下。改进的办法——用空间换时间,用数组保存中间状态,方便后面的计算。这就是动态规划(DP)的核心思想了。
DP求解LCS
用二维数组c[i][j]记录串x1x2⋯xi与y1y2⋯yj的LCS长度,则可得到状态转移方程

以s1=1,3,4,5,6,7,7,8,s2=3,5,7,4,8,6,7,8,2为例。我们借用《算法导论》中的推导图:

图中的空白格子需要填上相应的数字(这个数字就是c[i,j]的定义,记录的LCS的长度值)。填的规则依据递归公式,简单来说:如果横竖(i,j)对应的两个元素相等,该格子的值 = c[i-1,j-1] + 1。如果不等,取c[i-1,j] 和 c[i,j-1]的最大值。首先初始化该表:

S1的元素3 与 S2的元素5 不等,c[2,2] =max(c[1,2],c[2,1]),图中c[1,2] 和 c[2,1] 背景色为浅黄色。

继续填充:

至此,该表填完。根据性质,c[8,9] = S1 和 S2 的 LCS的长度,即为5。

本文S1和S2的最LCS并不是只有1个,本文并不是着重讲输出两个序列的所有LCS,只是介绍如何通过上表,输出其中一个LCS。

我们根据递归公式构建了上表,我们将从最后一个元素c[8][9]倒推出S1和S2的LCS。

c[8][9] = 5,且S1[8] != S2[9],所以倒推回去,c[8][9]的值来源于c[8][8]的值(因为c[8][8] > c[7][9])。

c[8][8] = 5, 且S1[8] = S2[8], 所以倒推回去,c[8][8]的值来源于 c[7][7]。

以此类推,如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,这里请都选择一个方向(之后遇到这样的情况,也选择相同的方向)。

这就是倒推回去的路径,棕色方格为相等元素,即LCS = 3,4,6,7,8,这是其中一个结果。

如果如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,选择另一个方向,会得到另一个结果。

即LCS =3,5,7,7,8。

构建c[i][j]表需要Θ(mn),输出1个LCS的序列需要Θ(m+n)。

参考:
https://blog.csdn.net/hrn1216/article/details/51534607
https://blog.csdn.net/u012102306/article/details/53184446

最长公共子序列(LCS)动态规划解题笔记

最长公共子序列(LCS)动态规划解题笔记

参考:
动态规划解最长公共子序列问题
动态规划 最长公共子序列 过程图解
动态规划基础篇之最长公共子序列问题

题意

子序列和最子串的区别在于子串需要连续,但子序列不需要,但仍需要保持顺序,可以理解为在原字符串中删除若干字符,剩下的序列就是子序列。
最长公共子序列,即两个字符串的所有子序列中最长的一个,当然可能会出现不止一个最长子序列的情况。

思路

一个关键点是公共子序列的长度,公共子序列有很多,求最长的,需要保存一下子序列的长度。
对于长度分别为len1和len2的两个字符串str1和str2,动态规划的着眼点在于,对于str1的下标i和str2的下标j:
- 如果两个字符串在相应上的字符相同,那么它们的最长公共子序列就是之前的最长公共子序列加上该字符,最大长度+1;
- 如果字符不相同,那么最长公共子序列就是以下二者中的长度最长的一个:
- str1[0:i]和str2[0:j-1]的最长公共子序列
- str1[0:i-1]和str2[0:j]的最长公共子序列
- 如果长度相等,走哪个分支都一样,但是可能对应不同的子序列,所以最长公共子序列并不唯一

同时在比较的过程中,可以得知每次的最长公共子序列的偏向(上面二者中的哪一个,即,虽然所有子序列的字符在两个字符串中都有,但当前添加的最后一个字符是明显来自于某个字符串的下标位置字符的(或者二者相等)),在循环结束后,可以据此从最后递归倒推到头部输出最大公共子序列。

代码实现

假设str1和str2的下标分别用i,j表示,创建两个二维数组,一个用于保存str1[0:i]和str2[0:j]的最长公共子序列的长度,另一个则表示在遍历过程中每次取的是哪一边的最长长度:

import java.lang.Math;
public class Test 
    static int[][] lens;
    static int[][] ores;
    static String result = "";
    public static String lcs(String x, String y) 
        // your code here
        int len1 = x.length();
        int len2 = y.length();
        // 因为要根据i-1和j-1位置的值来推断当前长度,数组需要比字符串长度大一号,
        // 并且把第i行和第j列都设为0,循环时从1开始,对应字符串的i-1和j-1位。
        lens = new int[len1+1][len2+1];
        ores = new int[len1+1][len2+1];
        for(int i = 0; i <= len1; i++) 
            lens[i][0] = 0;        
        
        for(int j = 0; j <= len2; j++) 
            lens[0][j] = 0;        
        
        for (int i = 1; i <= len1=; i++) 
            for (int j = 1; j <= len2=; j++) 
                // 获取该位置上的两个数字
                int xi = Integer.valueOf(x.charAt(i - 1));
                int yj = Integer.valueOf(y.charAt(j - 1));
                if (xi == yj) 
                    // 如果相同,最长公共子序列长度加一,没有偏向,可以直接输出该位置的字符
                    // 也就是x[0:i]相对于x[0:i-1],y[0:j]相对于y[0:j-1]都多了一个公共字符
                    lens[i][j] = lens[i-1][j-1] + 1;
                    ores[i][j] = 0;
                 else if (lens[i][j-1] > lens[i-1][j])
                    // 如果x[0:i]和y[0:j-1]的lsc较长,则赋值给它,并设置偏向
                    // 说明x[0:i]和y[0:j-1]相对于x[0:i-1]和y[0:j]多了一个公共字符
                    lens[i][j] = lens[i][j-1];
                    ores[i][j] = 1;
                 else 
                    // 反之如果x[0:i-1]和y[0:j]的lsc较长,则赋值给它,并设置偏向
                    // 说明x[0:i-1]和y[0:j]相对于x[0:i]和y[0:j-1]多了一个公共字符
                    lens[i][j] = lens[i-1][j];
                    ores[i][j] = -1;
                
            
        
//         输出
        return printLCS(x, len1, len2);
    

    public static void main(String[] args) 
        lcs("132535365", "123456789");
    

    static String printLCS(String x, int i, int j) 
        if (i == 0 || j == 0) 
            return "";
         
        if (ores[i][j] == 0) 
            // 直接添加该位置上的字符
            result = printLCS(x, i - 1, j - 1) + String.valueOf(x.charAt(i-1));
         else if (ores[i][j] == 1) 
            // 说明x[0:i]和y[0:j-1]相对于x[0:i-1]和y[0:j]多了一个公共字符
            // 因此保持i不变,j-1继续递归
            result = printLCS(x, i, j - 1) ;
         else 
            // 说明x[0:i-1]和y[0:j]相对于x[0:i]和y[0:j-1]多了一个公共字符
            // 因此保持j不变,i-1继续递归
            result = printLCS(x, i - 1, j) ;  
        
        return result;
    

一些优化解法(来自codewar其他人的解法)

public static String lcs(String x, String y) 
  // your code here
    int m = x.length(), n = y.length();
    int[][] nums = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) 
        for (int j = 1; j <= n; j++) 
            nums[i][j] = nums[i - 1][j - 1] + (x.charAt(i - 1) == y.charAt(j - 1) ? 1 : 0);
            nums[i][j] = Math.max(nums[i][j], nums[i - 1][j]);
            nums[i][j] = Math.max(nums[i][j], nums[i][j - 1]);
        
    
    StringBuilder sb = new StringBuilder();
    for(int i = 1; i <= n; i++) 
        if (nums[m][i] - nums[m][i - 1] == 1) 
            sb.append(y.charAt(i - 1));
        
    
    return sb.toString();


public static String lcs(String x, String y) 
    if (x.isEmpty() || y.isEmpty())
        return new String();
    if (x.charAt( x.length() - 1 ) == y.charAt( y.length() - 1 ))
        return lcs( x.substring( 0, x.length() - 1 ),
                y.substring( 0, y.length() - 1 ) ) + x.charAt( x.length() - 1 );
    String answerFory = lcs( x, y.substring( 0, y.length() - 1 ) );
    String answerForx = lcs( x.substring( 0, x.length() - 1 ), y );
    return answerFory.length() > answerForx.length() ? answerFory : answerForx;

以上是关于动态规划 最长公共子序列 过程图解的主要内容,如果未能解决你的问题,请参考以下文章

最长公共子序列(LCS)动态规划解题笔记

最长公共子序列(LCS)动态规划解题笔记

最长公共子序列(动态规划)

关于用动态规划法求最大公共子序列的问题

用数学语言说一下动态规划求数列最长递增子序列的解

动态规划 最长公共字符子序列