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

Posted yifanrensheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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经典动态规划:编辑距离

以上是关于DP-02动态规划算法题目解析的主要内容,如果未能解决你的问题,请参考以下文章

DP-03动态规划算法题目解析

区间型动态规划题目解析

深度解析「正则表达式匹配」:从暴力解法到动态规划

动态规划

算法题套路总结(三)——动态规划

动态规划分析总结——怎样设计和实现动态规划算法