zoukankan      html  css  js  c++  java
  • 算法--字符串:最长公共子序列LCS

    转载自:labuladong公众号

    最长公共子序列(Longest Common Subsequence,简称 LCS)是一道非常经典的面试题目,因为它的解法是典型的二维动态规划,大部分比较困难的字符串问题都和这个问题一个套路,比如说编辑距离。而且,这个算法稍加改造就可以用于解决其他问题,所以说 LCS 算法是值得掌握的。

    题目就是让我们求两个字符串的 LCS 长度:

    输入: str1 = "abcde", str2 = "ace" 
    输出: 3  
    解释: 最长公共子序列是 "ace",它的长度是 3

    肯定有读者会问,为啥这个问题就是动态规划来解决呢?因为子序列类型的问题,穷举出所有可能的结果都不容易,而动态规划算法做的就是穷举 + 剪枝,它俩天生一对儿。所以可以说只要涉及子序列问题,十有八九都需要动态规划来解决,往这方面考虑就对了。

    下面就来手把手分析一下,这道题目如何用动态规划技巧解决。

    一、动态规划思路

    第一步,一定要明确dp数组的含义。对于两个字符串的动态规划问题,套路是通用的。

    比如说对于字符串s1s2,一般来说都要构造一个这样的 DP table:

     

    为了方便理解此表,我们暂时认为索引是从 1 开始的,待会的代码中只要稍作调整即可。其中,dp[i][j]的含义是:对于s1[1..i]s2[1..j],它们的 LCS 长度是dp[i][j]

    比如上图的例子,d[2][4] 的含义就是:对于"ac""babc",它们的 LCS 长度是 2。我们最终想得到的答案应该是dp[3][6]

    第二步,定义 base case。

    我们专门让索引为 0 的行和列表示空串,dp[0][..]dp[..][0]都应该初始化为 0,这就是 base case。

    比如说,按照刚才 dp 数组的定义,dp[0][3]=0的含义是:对于字符串"""bab",其 LCS 的长度为 0。因为有一个字符串是空串,它们的最长公共子序列的长度显然应该是 0。

    第三步,找状态转移方程。

    这是动态规划最难的一步,不过好在这种字符串问题的套路都差不多,权且借这道题来聊聊处理这类问题的思路。

    状态转移说简单些就是做选择,比如说这个问题,是求s1s2的最长公共子序列,不妨称这个子序列为lcs。那么对于s1s2中的每个字符,有什么选择?很简单,两种选择,要么在lcs中,要么不在。

    这个「在」和「不在」就是选择,关键是,应该如何选择呢?这个需要动点脑筋:如果某个字符应该在lcs中,那么这个字符肯定同时存在于s1s2中,因为lcs是最长公共子序列嘛。所以本题的思路是这样:

    用两个指针ij从后往前遍历s1s2,如果s1[i]==s2[j],那么这个字符一定在lcs;否则的话,s1[i]s2[j]这两个字符至少有一个不在lcs,需要丢弃一个。先看一下递归解法,比较容易理解:

    对于第一种情况,找到一个lcs中的字符,同时将i, j向前移动一位,并给lcs的长度加一;对于后者,则尝试两种情况,取更大的结果。

    其实这段代码就是暴力解法,我们可以通过备忘录或者 DP table 来优化时间复杂度,比如通过前文描述的 DP table 来解决:

    二、疑难解答

    对于s1[i]s2[j]不相等的情况,至少有一个字符不在lcs中,会不会两个字符都不在呢?比如下面这种情况:

    所以代码是不是应该考虑这种情况,改成这样:

    if str1[i - 1] == str2[j - 1]:
        # ...
    else:
        dp[i][j] = max(dp[i-1][j], 
                       dp[i][j-1],
                       dp[i-1][j-1])

    我一开始也有这种怀疑,其实可以这样改,也能得到正确答案,但是多此一举,因为dp[i-1][j-1]永远是三者中最小的,max 根本不可能取到它。

    原因在于我们对 dp 数组的定义:对于s1[1..i]s2[1..j],它们的 LCS 长度是dp[i][j]

    这样一看,显然dp[i-1][j-1]对应的lcs长度不可能比前两种情况大,所以没有必要参与比较。

    三、总结

    对于两个字符串的动态规划问题,一般来说都是像本文一样定义 DP table,因为这样定义有一个好处,就是容易写出状态转移方程,dp[i][j]的状态可以通过之前的状态推导出来:

     找状态转移方程的方法是,思考每个状态有哪些「选择」,只要我们能用正确的逻辑做出正确的选择,算法就能够正确运行。

  • 相关阅读:
    改造vant日期选择
    css3元素垂直居中
    npm综合
    (转)网页加水印方法
    Mac下IDEA自带MAVEN插件的全局环境配置
    隐藏注册控件窗口
    High performance optimization and acceleration for randomWalk, deepwalk, node2vec (Python)
    How to add conda env into jupyter notebook installed by pip
    The Power of WordNet and How to Use It in Python
    背单词app测评,2018年
  • 原文地址:https://www.cnblogs.com/clarino/p/12416886.html
Copyright © 2011-2022 走看看