一、动态规划定义和基本思路
动态规划就是把复杂的问题分解为简单的子问题的求解方式。
动态规划的基本思路:通常用于求解具有某种最优性质的问题,试图只解决每个问题一次一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。动态规划自底向上(迭代而非递归)求解问题,避免重复计算。
而对于递归,求解这些子问题,然后合并子问题的解得到原问题的解。区别在于这些子问题会有重叠,一个子问题在求解后,可能会再次求解。
所以,我们可以将这些子问题的解存储起来,不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。下次使用时,直接把结果拿来用即可。这就是动态规划法的基本思路。
实例:
# 动态规划实现斐波那契数列
def fib(n):
# 递推公式的边界条件
if n == 0 or n == 1:
return n
dp = [0, 1, 0]
for i in range(1, n):
sum = dp[0] + dp[1]
dp[0] = dp[1]
dp[1] = sum
return dp[1]
print(fib(10))
# 时间复杂度:O(n)
# 空间复杂度:O(1)
# 递归思路
def fib(n):
if n == 0 or n == 1:
return n
return fib(n-1) + fib(n-2)
print(fib(10))
# 时间复杂度:O(2^n)
# 空间复杂度:O(n)
二、动态规划的基本步骤
1. 确定能够保存递归或递推公式得到的结果的数组以及下标的含义。一般使用一维数组(简单一些的题目)或二维数组(难度稍大一些)来保存,在这里一律用dp[]表示,其中,我们需要理解一维dp[i](数组对应索引位置的元素),二维数组dp[i][j](二维数组中,通俗讲就是行和列对应索引位置的元素);
2.dp数组初始化。对于一维数组和二维数组,初始值能够帮助下次出现结果进行叠加的效果。
比如:斐波那契dp[0] = 0,dp[1] = 1,那么dp[2] = dp[0] + dp[1].
3. 确定状态转移方程(递推公式)。状态转移就是根据上一阶段的状态来导出本阶段的状态(即不同阶段之间的联系),其实这里就是,在之前用到的结果,能否在本阶段是否适用。
比如:斐波那契的fib(4)递归出现两次计算,都是相同的计算方式fib(4) = fib(3) + fib(2),所以这种计算方式就是为递推公式。而我们动态规化就将第一次的计算结果保存,下次出现就直接使用结果。
4.确定遍历顺序。比如:斐波那契的遍历就是从前到后的,确定遍历的顺序能够更快的得出结果。
5. 举例dp数组。在完成以上步骤,我们就可以举出一些例子,进行推导,又或者在完成代码后,我们可是使用代码编辑器,我使用的是vscode,pycharm进行代码 debug(调试)。
在这里,用力扣72. 编辑距离 - 力扣(LeetCode)来解释一下这五个步骤。
第一步:确定数组dp,建立一个m+1行n+1列的数组用来保存每一次计算的操作数。因为是存在word1,word2进行相关的操作,所以我们可以使用二维数组将word1转换成word2所使用的最少操作数保存。
dp = [[0] * (n+1) for i in range(m+1)
在这里,需要理解下标dp[i][j]的含义:word1的前i个字符(下标i-1)和word2的前j个字符(下标j-1)的最少编辑距离,例如:word1 = 'op',word2= 'app',那么dp[2][2]=X,表示'op'和'ap'的最少编辑距离,即把'op'转换为'ap'最少需要多少操作。
第二步:确定数组dp的初始值。在表格中实现初始化,就是要理解dp[i-1][j-1]即dp[i][j]的联系了。
例如:如果我们要找到word1和word2对应的第一个字符dp[i][j]进行操作,我们就需要找到上一步操作的最少的操作数即dp[i-1][j-1],然而,对应的元素确是空,所以我们确定初始值时,需要考虑dp[i-1][j-1] = 0的情况。
那么,现在我们就可以确定初始值的第一行元素就是空字符经过添加操作变成word2的操作数,第一列的元素就是word1经过删除操作变成空字符串的操作数。
行:word1 列:word2 |
“ ” 空 |
a | ap | app | appl | apple |
“ ” 空 |
0 | 1 | 2 | 3 | 4 | 5 |
o | 1 | |||||
op | 2 | |||||
opp | 3 | |||||
oppa | 4 |
dp[0][0] = 0 # 第一个元素赋值为0
for i in range(m+1): # 对word1里的元素全部做删除操作
dp[i][0] = i
for j in range(n+1): # 对word2里的元素进行添加操作
dp[0][j] = j
第三步、确定递推公式。这道题中,难度大的就是递推公式,在理解递推公式之前,我们先判断可能出现的情况:
1、 如果在转换时word1[i-1] == word2[j-1],我们就不需要进行操作了,我们就可以将下标dp[i-1][j-1]保留的结果直接赋给本次,即dp[i][j] == dp[i-1][j-1]。
2、word1[i-1] != word2[j-1]时,我们就需要考虑三种操作出现的方式,
即:word1替换word1[i-1]:dp[i][j] = dp[i-1][j-1] + 1
word2删除一个元素:dp[i][j] = dp[i][j-1] + 1
word1删除元素:dp[i][j] = dp[i-1][j] + 1 , 相当于word2添加一个元素。
dp[i-1][j-1] | dp[i-1][j] |
dp[i][j-1] | dp[i][j] |
在这里,为什么要 + 1 呢?
是因为我们在这三次操作方式中都是dp数组中保留的上一次的结果,我们需要求解出 dp[i][j] 的结果。最后取得操作数的最小值(最少操作数)进行 +1。
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j-1],dp[i][j-1],dp[i-1][j])+1
第四步、确定数组的遍历顺序,很明显,我们需要结合四个递推公式,可得知遍历顺序为从左到右和从上到下进行遍历。
第五步、通过实例,进行验证推导
这里使用word1=‘oppa’和‘apple’进行推导
行:word1 列:word2 |
“ ” 空 |
a | ap | app | appl | apple |
“ ” 空 |
0 | 1 | 2 | 3 | 4 | 5 |
o | 1 | 1 | 2 | 3 | 4 | 5 |
op | 2 | 2 | 1 | 2 | 3 | 4 |
opp | 3 | 3 | 2 | 1 | 2 | 3 |
oppa | 4 | 3 | 3 | 2 | 2 | 3 |
可以说,大部分的动态规划算法题都基于这五个步骤,所以,一定要注意五个步骤的含义。
我们还需要多去刷一些题,能够更好的去理解动态规划的思路。
python代码实现:
def minDistance(word1, word2):
m = len(word1)
n = len(word2)
# 当word1为空的时候,word2的长度就是需要编辑的距离
if m == 0:
return n
# 当word2为空的时候,word1的长度就是需要编辑的距离
if n == 0:
return m
# 创建一个m+1行n+1列的数组
dp = [[0]*(n+1) for i in range(m+1)]
dp[0][0] = 0
# 从 word1[:i] 变成 空串(word2[:0]), 操作即**删除** i 次直到将 word1[:i] 删光到 空串
for i in range(1,m+1):
dp[i][0] = i
# 从 空串(word1[:0]) 变成 word2[:j], 操作即**插入** j 次直到将 空串 变成 word2[:j]
for j in range(1,n+1):
dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
# 考虑字符是否相等
if word1[i-1] == word2[j-1]:
# 如果相等就是上一步操作的步数
dp[i][j] = dp[i-1][j-1]
else:
# 如果不相等就是附近三个最小值+1
dp[i][j] = 1+min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])
return dp[m][n]
s1 = "oppa"
s2 = "apple"
print(minDistance(s1,s2))
在此,感谢大家的浏览,同时,感谢各位优秀的大佬提供的资料,让我明白了动态规划。
如有相同文章,请告知作者,附上链接,我在本文章相应的位置附上链接,谢谢。