• 【DP-02】动态规划算法题目解析


    目录

    1. 最长公共子序列
    2. 编辑距离
    3. 最长上升子序列

    结合上一篇文章,再继续尝试解决动态规划题目

    一、1143. 最长公共子序列

    1.1 问题:

    给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度

    一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

    例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

    若这两个字符串没有公共子序列,则返回 0。

    示例 1:

    输入:text1 = "abcde", text2 = "ace"

    输出:3

    解释:最长公共子序列是 "ace",它的长度为 3。

    示例 2:

    输入:text1 = "abc", text2 = "abc"

    输出:3

    解释:最长公共子序列是 "abc",它的长度为 3。

    示例 3:

    输入:text1 = "abc", text2 = "def"

    输出:0

    解释:两个字符串没有公共子序列,返回 0。

    1.2 求解:

    1)步骤一:定义子问题

    要定义子问题,我们还是抓住这样一个子问题的基本性质:子问题是和原问题相似,但规模较小的问题。本体属于二维动态规划题目。

    f(i,j) 表示长度为i和j的两个字符串的公共子串长度。

    2)写出子问题的递推关系

    这一步是求解动态规划问题的关键。二维的子问题有很多可能的递推关系,有些题目一目了然,有些则可能需要仔细推敲。 一般来说,我们首先思考能不能使用一种最简单的子问题递推关系:看当前子问题和前一个子问题的关系。如果是一维子问题,就是看 f(i)和 f(i-1)的关系;如果是二维子问题,则是看f(i,j)f(i-1,j) f(i,j-1)f(i-1,j-1) 的关系。LCS 问题就是这种简单递推关系的代表。

    情况一:

    情况二:

    这样,我们得到的子问题递推关系为:

    注意这里涉及到边界值:

    3)确定 DP 数组的计算顺序

    对于二维动态规划问题,我们仍然要坚持使用 DP 数组,用自底向上的顺序计算子问题。因为 DP 数组中的每一个元素都对应一个子问题,当子问题变成二维之后,DP 数组也需要是二维数组。在 DP 数组中,

    Dp[i][j]对应子问题f(i,j)的值。

    但是对于二维动态规划问题,我们需要有一定的方法来思考 DP 数组的计算顺序。

    DP 数组计算顺序的基本原则是:当我们计算一个子问题时,它所依赖的其他子问题应该已经计算好了。 根据这个原则,我们思考三点内容。

    第一点:DP 数组的有效范围是什么?

    因此 dp = [[0]*(n+1) for _ in range(m+1)] 。定义数组为[m+1][n+1].

    第二点:base case 和原问题在 DP 数组中在什么位置? 如下图所示,base case 位于 DP 数组的最左侧一列和最上方一行,而原问题则位于 DP 数组的右下角。

    第三点:DP 数组的子问题依赖方向是什么? 观察子问题的递推关系,f(i,j)依赖:f(i-1,j) f(i,j-1)f(i-1,j-1)

    我们发现,子问题的依赖方向是向右、向下的,因此 DP 数组的计算顺序也应该是从左到右、从上到下。也就是说我们应该以这样的顺序遍历 DP 数组:

    for i in range(1,m+1):
    for j in range(1,n+1):

    具体代码见1.3部分。

    4 )空间优化(可选)

    二维动态规划问题的 DP 数组变成了二维数组,空间复杂度更高了。因此,二维动态规划问题也更值得进行空间优化,降低空间复杂度。

    不过,二维动态规划问题的空间优化有很多种方法,需要根据不同的情况灵活使用。空间优化的步骤是可选的,优化不优化都可以。 本题进行垂直方向压缩,也即是只取n+1维数组,如下图所示,具体代码见1.3部分。

    最终变成以下表达式,后续根据这个向右滚动。

    last

    temp

    dp[j-1]

    dp[j-1]

    需要注意的是,空间优化方法只能优化空间复杂度,不能优化时间复杂度。例如 LCS 问题在空间优化前后的复杂度为:

    1.3 代码

    1)优化前

    class Solution(object):
        def longestCommonSubsequence(self, text1, text2):
            """
        子问题:
         f(i, j) = s[0..i) 和 t[0..j) 的最长公共子序列
         f(0, *) = 0
         f(*, 0) = 0
         f(i, j) = f(i-1, j-1) + 1, if s[i-1] == t[j-1]
            max{ f(i-1, j), f(i, j-1) }, otherwise
            """
            if not text1 or not text2:
                return 0
            m = len(text1)
            n = len(text2)
            dp = [[0]*(n+1) for _ in range(m+1)] #[m+1][n+1]的矩阵
            for i in range(1,m+1):
                for j in range(1,n+1):
                    if text1[i-1] == text2[j-1]:
                        dp[i][j] = 1 + dp[i-1][j-1]
                    else:
                        dp[i][j] = max(dp[i-1][j],dp[i][j-1])
            return dp[m][n]

    2)优化后

    class Solution(object):
        def longestCommonSubsequence(self, text1, text2):
            """
        子问题:
         f(i, j) = s[0..i) 和 t[0..j) 的最长公共子序列    f(0, *) = 0   f(*, 0) = 0
         f(i, j) = f(i-1, j-1) + 1, if s[i-1] == t[j-1]    max{ f(i-1, j), f(i, j-1) }, otherwise
            """
            if not text1 or not text2:
                return 0
            m = len(text1)
            n = len(text2)
            dp = [0]*(n+1)
            # temp = 0
            for i in range(1,m+1):
                last = 0 
                for j in range(1,n+1):
                    temp =dp[j]
                    if text1[i-1] == text2[j-1]:
                        dp[j] = last + 1
                    else:
                        dp[j] = max(temp,dp[j-1])
                    last = temp  #向前滚动,temp的值赋值给last
            return dp[n]

    二、leetcode72. 编辑距离

    2.1 问题:

    给你两个单词 word1  word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 

    你可以对一个单词进行如下三种操作:

    插入一个字符

    删除一个字符

    替换一个字符

    示例 1

    输入:word1 = "horse", word2 = "ros"

    输出:3

    解释:

    horse -> rorse ( 'h' 替换为 'r')

    rorse -> rose (删除 'r')

    rose -> ros (删除 'e')

    示例 2

    输入:word1 = "intention", word2 = "execution"

    输出:5

    解释:

    intention -> inention (删除 't')

    inention -> enention ( 'i' 替换为 'e')

    enention -> exention ( 'n' 替换为 'x')

    exention -> exection ( 'n' 替换为 'c')

    exection -> execution (插入 'u')

    2.2 求解:

    该问题较难,先分析如下:

    如果你觉得从全局考虑很困难,就试试先不考虑全局,从局部入手。我们可以只考虑其中的「一步」,至于剩下的步骤,就交给其他子问题完成就行。对于编辑距离来说,这「一步」就是指「单次的编辑操作」。

    这有点类似递归的思路。我只需要把当前这一步计算做好,然后相信递归函数能帮我做好剩下的计算。动态规划其实很像递归,只不过动态规划一般是自底向上计算,保存每个子问题。

    1)步骤一:定义子问题

       

    2)写出子问题的递推关系

    dp[i][j] 代表 word1 i 位置转换成 word2 j 位置需要最少步数所以,

    情况一,如下图当 word1[i] != word2[j]dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1

    情况二,如下图所示:当 word1[i] == word2[j]dp[i][j] = dp[i-1][j-1]

    其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。补充理解如下:

    以 word1 为 "horse",word2 为 "ros",且 dp[5][3] 为例,即要将 word1的前 5 个字符转换为 word2的前 3 个字符,也就是将 horse 转换为 ros,因此有:

    (1) dp[i-1][j-1],即先将 word1 的前 4 个字符 hors 转换为 word2 的前 2 个字符 ro,然后将第五个字符 word1[4](因为下标基数以 0 开始) 由 e 替换为 s(即替换为 word2 的第三个字符,word2[2])

    (2) dp[i][j-1],即先将 word1 的前 5 个字符 horse 转换为 word2 的前 2 个字符 ro,然后在末尾补充一个 s,即插入操作

    (3) dp[i-1][j],即先将 word1 的前 4 个字符 hors 转换为 word2 的前 3 个字符 ros,然后删除 word1 的第 5 个字符

       

    这样,我们得到最终的子问题递推关系为:

    注意这里涉及到边界值:

    f(0,j) = j

    f(i,0) =i

    3)确定 DP 数组的计算顺序

    和第一章类似,f(i,j)依赖:f(i-1,j) f(i,j-1)f(i-1,j-1)

    具体代码可见2.3

    4 )空间优化(可选)

    编辑距离问题本身属于较难的题目,所以我们写出基本的解法就可以,一般面试中不会追问空间优化的方法。

    2.3 代码

    class Solution:
        def minDistance(self, word1: str, word2: str) -> int:
            n1 = len(word1)
            n2 = len(word2)
            dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]   
            # 第一行,初始化
            for j in range(1, n2 + 1):
                dp[0][j] = dp[0][j-1] + 1
            # 第一列,初始化
            for i in range(1, n1 + 1):
                dp[i][0] = dp[i-1][0] + 1
            for i in range(1, n1 + 1):
                for j in range(1, n2 + 1):
                    if word1[i-1] == word2[j-1]:
                        dp[i][j] = dp[i-1][j-1]
                    else:
                        dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1
            #print(dp)      
            return dp[-1][-1]

    三、leetcode300. 最长上升子序列

    3.1 问题:

    给定一个无序的整数数组,找到其中最长上升子序列的长度。

    示例:

    输入: [10,9,2,5,3,7,101,18]

    输出: 4

    解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4

    说明:

    可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

    你算法的时间复杂度应该为 O(n^2)

    进阶: 你能将算法的时间复杂度降低到 O(n log n) ?

    注意:这里不用紧邻,只要前后关系即可。

    3.2 求解:

    1)步骤一:定义子问题

    每个问题可以看成规模更小的子问题,使用DP[i]表示numsi个数字的最长子序列长度。

    2)写出子问题的递推关系

    每次可能用到所有的dp[i]的数据。

    3)确定 DP 数组的计算顺序

    根据当前i的值,和递归后的值进行比较,取最大的。

    4 )空间优化(可选)

    每次都要用到之前的数据,本题不可优化。

    3.3 代码

    # Dynamic programming.
    class Solution:
        def lengthOfLIS(self, nums: List[int]) -> int:
            if not nums: return 0
            dp = [1] * len(nums)
            for i in range(len(nums)):
                for j in range(i):
                    if nums[j] < nums[i]: # 如果要求非严格递增,将此行 '<' 改为 '<=' 即可。
                        dp[i] = max(dp[i], dp[j] + 1)
            return max(dp)

    参考文献:

    【1】 最长公共子序列:二维动态规划的解法

    2经典动态规划:编辑距离

  • 相关阅读:
    在Vue脚手架里面使用font-awsome
    在webstorm上使用git
    smartGit继续使用的方法
    工作笔记
    “老司机”传授给“小白”的职业经验
    兼容性问题(目前遇到的)
    web前端页面项目经验总结
    jquery中隐藏div的几种方法
    懒加载和预加载
    JS 中的事件绑定、事件监听、事件委托
  • 原文地址:https://www.cnblogs.com/yifanrensheng/p/12940854.html
Copyright © 2020-2023  润新知