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

Posted zhu_free

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最长公共子序列(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)

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

动态规划-最长公共子序列LCS

动态规划算法解最长公共子序列LCS问题

1006 最长公共子序列Lcs(经典动态规划)