动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推的方式去解决。
决策变量是随着状态变量变化而变化,因此决策是动态的。最终的全局最优解是规划出来的。
使用动态规划需满足无后效性,即某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。对无后效性的解释具体看https://blog.csdn.net/Chasing__Dreams/article/details/108316722
类似于高中数学中的数列,可以说是魔改版数列。
高中的题一般需先推导出状态转移方程,再据此推导出显式表达式(在高中时代称为通项公式),然而,动态规划是要我们在推导出状态转移方程后,根据状态转移方程用计算机暴力求解出来。显式表达式?在动态规划中是不存在的。
递推公式
二维数组
首行首列的初始化。二维空间元素代表的是一个最优子问题的解,后续的最优子问题的解的计算可能会用到之前已计算出的最优子问题的解。通常该二维数组可进行优化
,因动态规划的无后向性使得在计算当前最优子问题时,只会用到之前某一行或个别行,因此,对用不到的行可以不用保存。并且在某些动态规划问题中,如果在计算当前最优子问题时,只会用到之前某一行或个别行某几个所处为止有规律可循的元素时,可以继续对空间优化
,只需开辟所需的几个空间即可。
若此时使用递归(自顶向下)会造成对子问题的重复计算,使得时间复杂度是指数级的。
为避免(1)中对子问题的重复计算,可以对子问题的计算结果进行保存,以空间换时间,降低时间复杂度。
此时使用的仍是递归方法,称为备忘录算法。
(2)中时间复杂度已降到最低,但空间复杂度仍较高可能是O(n)或O(m*n)等,应该对问题进行进一步分析,若使用for循环自底向上迭代,每次迭代使用的是以往哪些记录,只需用有限的空间对其保存,来降低空间复杂度。
详细解析看该篇文章中的上楼梯问题的三种解决方法
解析过程参考[应用动态规划——将动态规划拆分成三个子目标][https://www.zhihu.com/question/39948290]
# fib.py
import numpy as np
def fib(n):
"""
时间复杂度:
O(n)
空间复杂度:
O(n)
:param n:
:return:
"""
f_n = np.zeros(n)
f_n[1] = 1
for i in range(2, n):
f_n[i] = f_n[i-1] + f_n[i-2]
return f_n[n-1]
def fib_(n):
"""
时间复杂度:
O(n)
空间复杂度:
O(1)
:param n:
:return:
"""
x = 0
y = 1
for i in range(2, n):
z = y + x
y, x = z, y
return z
if __name__ == '__main__':
result = fib(10)
print(result)
result = fib_(10)
print(result)
"""
运行结果:
34.0
34
Process finished with exit code 0
"""
给定一组多个(n)物品,每种物品都有自己的重量(wi)和价值(vi),在限定的总重量/总容量(C)内,选择其中若干个(也即每种物品可以选0个或1个),设计选择方案使得物品的总价值最高。
解析过程可参考:
背包问题——“01背包”详解及实现(包含背包中具体物品的求解)
0-1背包问题的定性尤其该博文中的所谓“填一维表”的动态规划方法部分,可便于理解对二维空间压缩为一维时为什么从后往前计算。
# knapsack_0_1.py
from collections import deque
from typing import List, Tuple
import numpy as np
def knapsack_0_1(w_vs: List[Tuple[int, int]], heavy: int):
"""
0-1背包问题
状态转换方程构建过程:
(1) 原问题数学描述:
f(i, C(i))表示在前i个物品中挑选总重量不超过C(i)且总价值最大的商品
(2) 子问题数学描述:
f(i-1, C(i-1))表示在前i-1个物品中挑选总重量不超过C(i-1)且总价值最大的商品
(3) 构建递推公式:
f(i, C(i)) = max(f(i-1, C(i)), f(i-1, C(i)-w[i]))
其中C(i)与C(i-1)之间的关系:
w[i] > C(i) 时,C(i-1) == C(i)
w[i] > C(i) 时,C(i-1) = C(i)-w[i])
(4) 边界:
i = 0时,f(i, C(i)) = 0
C(i) = 0时,f(i, C(i)) = 0
w[i] > C(i) 时, f(i, C(i)) = f(i-1, C(i-1))
(5) 构建状态转换方程
使用缓存max_value[][]记录价值
使用循环自底向上
时间复杂度:
O(len(w_vs)*heavy)
空间复杂度:
O(len(w_vs)*heavy)
:param w_vs: 物品质量与价值的元组列表
:param heavy: 背包所能装载的重量
:return: 背包所能装的物品最大价值,以及所装的物品
"""
w = deque([0]) # 存放所有物品的重量
v = deque([0]) # 存放所有物品的价值
for w_, v_ in w_vs: # 初始化w和v数组
w.append(w_)
v.append(v_)
goods_num = len(w_vs) # 物品的数量
max_value = np.zeros([goods_num+1, heavy+1])
for i in range(1, goods_num+1): # i为阶段变量,表示当前第几个物品
for j in range(1, heavy+1): # j为决策变量,表示从i个物品中挑选出的物品总重量不超过j
if w[i] > j:
# 当前物品的重量超过当前所要求的总重量j
max_value[i][j] = max_value[i-1][j]
else:
# 当前物品的重量未超过当前所要求的总重量j
a = max_value[i-1][j]
b = max_value[i-1][j - w[i]] + v[i]
if a > b:
# 背包中未装当前物品i
max_value[i][j] = a
else:
# 背包中装入当前物品i
max_value[i][j] = b
path = deque() # 用于存放所装物品
i = goods_num
j = heavy
while i > 0 and j > 0:
if max_value[i][j] == max_value[i-1][j - w[i]] + v[i]:
# 表示背包装了第i个物品
path.appendleft(i)
j -= w[i]
i -= 1 # 不论背包装没装第i个物品,i都需-1
return max_value[goods_num][heavy], path # 背包所能装的物品最大价值,以及所装的物品
def knapsack_0_1_(w_vs: List[Tuple[int, int]], heavy):
"""
该方法是压缩了时间复杂度和空间复杂度
时间复杂度:
O(∑(i=1→n)(heavy-w[i]+1)) < O(len(w_vs)*heavy)
空间复杂度:
没有计算背包所装物品时为O(heavy))
计算背包所装物品时O(heavy+len(w_vs)*heavy)
:param w_vs: 物品质量与价值的元组列表
:param heavy: 背包所能装载的重量
:return: 背包所能装的物品最大价值,以及所装的物品
"""
w = deque([0]) # 存放所有物品的重量
v = deque([0]) # 存放所有物品的价值
for w_, v_ in w_vs: # 初始化w和v数组
w.append(w_)
v.append(v_)
goods_num = len(w_vs) # 物品的数量
path_ = np.zeros([goods_num+1, heavy+1]) # 用于表示是否装入第i个物品,装入时使path_[i][j] = 1
max_value = np.zeros(heavy + 1) #
for i in range(1, goods_num + 1): # i为阶段变量
# j为决策变量, 必须从heavy开始,如果正序遍历则当求max_value[j]时其前面的max_value[0],max_value[1],…,
# max_value[j-1]都已经改变过,里面存的都不是i-1时刻的值,这样求max_value[j]时利用max_value[j-w[i]]必定
# 是错的值。最后max_value[heavy]即为最大价值。
for j in range(heavy, w[i]-1, -1):
if w[i] <= j:
# 当前物品的重量未超过当前所要求的总重量j
a = max_value[j]
b = max_value[j - w[i]] + v[i]
if a < b:
# 背包中装入当前物品i
max_value[j] = b
path_[i][j] = 1 # 表示背包中装入物品i
path = deque() # 用于存放所装物品
i = goods_num
j = heavy
while i > 0 and j > 0:
if path_[i][j] == 1:
# 表示背包装了第i个物品
path.appendleft(i)
j -= w[i]
i -= 1 # 不论背包装没装第i个物品,i都需-1
return max_value[heavy], path # 背包所能装的物品最大价值,以及所装的物品
if __name__ == '__main__':
# 测试knapsack_0_1
w_vs = [(1, 1), (2, 6), (5, 18), (6, 22), (7, 28)]
heavy = 11
max_value = knapsack_0_1(w_vs, heavy)
print(max_value)
# 测试knapsack_0_1_
w_vs = [(1, 1), (2, 6), (5, 18), (6, 22), (7, 28)]
heavy = 11
max_value = knapsack_0_1_(w_vs, heavy)
print(max_value)
"""
运算结果:
(40.0, deque([3, 4]))
(40.0, deque([3, 4]))
Process finished with exit code 0
"""
有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。
解析过程参考[漫画:什么是动态规划?][https://zhuanlan.zhihu.com/p/31628866]
与斐波那契数列类似
# climb_staircase.py
import numpy as np
def climb_staircase(n):
ways = np.zeros(n+1)
ways[1] = 1
ways[2] = 2
for i in range(3, n+1):
ways[i] = ways[i-1] + ways[i-2]
return ways[n]
def climb_staircase_(n):
x = 1
y = 2
for i in range(3, n+1):
z = x + y
y, x = z, y
return z
if __name__ == '__main__':
result = climb_staircase(10)
print(result)
result = climb_staircase_(10)
print(result)
"""
运行结果:
89.0
89
Process finished with exit code 0
"""
有一个国家发现了m座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是n人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
输入:List[Tuple[人数,金数]],如:[(5, 500), (3, 200), (4, 300), (3, 350), (5, 400)]
输出:Tuple[最大金数,所挖金矿](900.0, deque([1, 5]))
该问题与0-1背包是同一个问题,代码一模一样。