动态规划基本思想:
将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。
动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法可以解决各类最优化问题。
下面用一个例子来体现动态规划思想:
在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。
看到这个题,我认为比较容易理解的方法是递归,就首先拿python代码实现递归:
L = [[7],
[3, 8],
[8, 1, 0],
[2, 7, 4, 4],
[4, 5, 2, 6, 5]]
length = len(L) - 1
def maxSum(r, c):
'''
功能:从二维列表L 数字三角形中找出一条路径,
使得路径上的所有数字之和最大
参数:r: 行
c: 列
返回:最大的数字之和
'''
if r == length:
return L[r][c]
x = maxSum(r + 1, c)
y = maxSum(r + 1, c + 1)
return max(x, y) + L[r][c]
print(maxSum(0,0))
以上代码输出结果 30。但是以上代码有大量的重复计算,经过同一个数的每条路径都会重新计算一遍,浪费了大量的时间。时间复杂度达到了2的n次方,很浪费时间。接下来进行改进代码,如果我们把之前计算过的数据进行存储,然后下一次不再进行计算,而直接使用,就会免去重复计算,节省好多时间。三角形总共有n(n+1)/2个数字,时间复杂度为n方。
但是这样我们还是使用了递归(只是记忆的),因为递归需要消耗大量的内存空间,所以我们还需要改进代码,不使用递归来完成。如果我们从最后一行往前推,倒数第二行的数加上倒数第一行的数即:倒数第二行的2加最后一行的4和5取最大值 max((2+4),(2+5)) =7, max((7+5),(7+2))=12,... ...以此类推把倒数第二行填上这些数保存,即左图。前三行一样,相加取最大值并保存(右图),最后得出的第一行的第一个数便是咱们上个程序得出的结果。
将以上的过程使用Python代码实现,这便是使用动态规划的思想实现的代码,分解成小问题,解决小问题,用小问题来进一步解决大问题
import numpy as np
L = [[7],
[3, 8],
[8, 1, 0],
[2, 7, 4, 4],
[4, 5, 2, 6, 5]]
length = len(L) - 1
# 此处使用了numpy的创建数组函数,以便存储之后求的值
maxS = np.zeros((length + 1, length + 1))
def maxSum():
for i in range(length):
maxS[length][i] = L[length][i]
for i in range(length - 1, -1, -1):
for j in range(i + 1):
maxS[i][j] = max(maxS[i + 1][j], maxS[i + 1][j + 1]) + L[i][j]
# 这里返回的值便是最后算出的结果
return maxS[0][0]
print(maxSum())
代码还可以继续进行优化,不必使用二维数组,使用一维的足够了。如下图所示递推,得到的第一个数便是所要求的数。
虽然空间上得到了优化,但是时间复杂度不变。
简化后的代码如下:
def maxSum():
maxS = L[length]
for i in range(length - 1, -1, -1):
for j in range(i + 1):
maxS[j] = max(maxS[j], maxS[j + 1]) + L[i][j]
return maxS[0]
这个动态规划的例子到这里已经完成了。做完这个例子,基本了解了动态规划的思想,但是涉及动态规划的问题很多,每种问题的解决办法不一样,还需要具体问题具体分析,我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。