动态规划(Dynamic programming)
是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推的方式去解决,当前子问题的解将由上一个子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
动态规划中的子问题往往不是相互独立的(即子问题重叠)。在求解的过程中,许多子问题的解被反复地使用。为了避免重复计算,动态规划算法采用了填表来保存子问题解的方法,即带备忘录的递归,当需要某个子问题的解时,直接取值即可,从而避免重复计算。
适用问题
符合“一个模型三个特征”的问题。
“一个模型”:指 多阶段决策最优解模型;
“三个特征”:分别是最优子结构、无后效性和重复子问题。
(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势。
这类问题的求解步骤通常如下:
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
(1)划分:按照问题的特征,把问题分为若干阶段。注意:划分后的阶段一定是有序的或者可排序的
(2)确定状态和状态变量:将问题发展到各个阶段时所处的各种不同的客观情况表现出来。状态的选择要满足无后续性
(3)确定决策并写出状态转移方程:状态转移就是根据上一阶段的决策和状态来导出本阶段的状态。根据相邻两个阶段状态之间的联系来确定决策方法和状态转移方程
(4)边界条件:状态转移方程是一个递推式,因此需要找到递推终止的条件
动态规划三要素:
(1)问题的阶段
(2)每个阶段的状态
(3)相邻两个阶段之间的递推关系
整个求解过程可以用一张最优决策表来描述,最优决策表是一张二维表(行:决策阶段,列:问题的状态)表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
经典问题1:斐波那契数列
def fibonacci(n):
if n < 2:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 1)
def dyna_fibonacci(n):
if n < 2:
return n
else:
a, b = 0, 1
for _ in range(n - 1):
a, b = b, a + b
return b
if __name__ == '__main__':
t1 = time.time()
fibonacci(100)
t2 = time.time()
fibonacci(100)
t3 = time.time()
print("Time required for using recursion:", t2 - t1)
print("Dynamic Planning Duration:", t3 - t2)
经典问题2:求两个字符串最大子串
def longest_subst(str1, str2):
maxlen, index = 0, 0
arr = [[0 for _ in range(len(str2) + 1)] for _ in range(len(str1) + 1)]
for i in range(1, len(str1) + 1):
for j in range(1, len(str2) + 1):
if str1[i - 1] == str2[j - 1]:
arr[i][j] = arr[i - 1][j - 1] + 1
else:
arr[i][j] = 0
if arr[i][j] > maxlen:
maxlen = arr[i][j]
index = j
return str2[index - maxlen:index]
print(longest_subst("1AB2345CD", "12345EF"))
经典问题3:背包问题
def bag_dynamic(weight, value, capacity):
n = len(weight)
weight.insert(0, 0)
value.insert(0, 0)
x = [] # 装入背包的物体的索引
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, capacity + 1):
if j >= weight[i]:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
else:
dp[i][j] = dp[i - 1][j]
j = capacity
for i in range(n, 0, -1):
if dp[i][j] > dp[i - 1][j]:
x.append(i)
j = j - weight[i]
print(x)
print(dp[n][capacity])
# n个物体的重量(w[0]无用)
weight = [1, 2, 5, 6, 7, 9]
value = [1, 6, 18, 22, 28, 36]
# 计算n的个数
n = len(weight) - 1
# 背包的载重量
capacity = 13
bag_dynamic(weight, value, capacity)
经典问题4:矩阵求和
题目主要信息:
给定一个矩阵,从矩阵左上角到右下角,每次只能向下或者向右
从左上角到右下角路径上经过的所有数字之和为路径和,求该路径和的最小值
矩阵不为空,每个元素值都为非负数
具体做法:
step 1:我们可以构造一个与矩阵同样大小的二维辅助数组,其中dp[i][j]dp[i][j]dp[i][j]表示以(i,j)(i,j)(i,j)位置为终点的最短路径和,则dp[0][0]=matrix[0][0]dp[0][0] = matrix[0][0]dp[0][0]=matrix[0][0]。
step 2:很容易知道第一行与第一列,只能分别向右或向下,没有第二种选择,因此第一行只能由其左边的累加,第一列只能由其上面的累加。
step 3:边缘状态构造好以后,遍历矩阵,补全矩阵中每个位置的dp数组值:如果当前的位置是(i,j)(i,j)(i,j),上一步要么是(i−1,j)(i-1,j)(i−1,j)往下,要么就是(i,j−1)(i,j-1)(i,j−1)往右,那么取其中较小值与当前位置的值相加就是到当前位置的最小路径和,因此状态转移公式为dp[i][j]=min(dp[i−1][j],dp[i][j−1])+matrix[i][j]dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + matrix[i][j]dp[i][j]=min(dp[i−1][j],dp[i][j−1])+matrix[i][j]。
step 4:最后移动到(n−1,m−1)(n-1,m-1)(n−1,m−1)的位置就是到右下角的最短路径和。
def max_Matrix(arr):
rows = len(arr)
cols = len(arr[0])
# dp[i][j]表示以当前i,j位置为终点的最短路径长度
dp = [[0 for _ in range(cols)] for _ in range(rows)]
dp[0][0] = arr[0][0]
# 处理第一行
for j in range(1, cols):
dp[0][j] = arr[0][j] + dp[0][j - 1]
# 处理第一列
for j in range(1, rows):
dp[j][0] = arr[j][0] + dp[j - 1][0]
print(dp)
for i in range(1, rows):
for j in range(1, cols):
if dp[i - 1][j] < dp[i][j - 1]:
dp[i][j] = dp[i - 1][j] + arr[i][j]
else:
dp[i][j] = dp[i][j - 1] + arr[i][j]
return dp[rows - 1][cols - 1]