动态规划算法将待求解问题拆分成一系列相互交叠的子问题,通过递推关系定义各子问题的求解策略,并随时记录子问题的解,最终获得原始问题的解,避免了对交叠子问题的重复求解。
在动态规划算法中有三要素:
- 最优子结构: 是指每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到
- 边界: 是指问题最小子集的解
- 状态转移函数: 是指从一个阶段向另一个阶段过渡的具体模式,描述的是两个相邻子问题之间的关系
- 爬楼梯问题
- 背包问题
- 最长递归子序列问题
爬楼梯问题
问题描述
现有一个10层台阶的楼梯,每次可以选择一步走一级台阶或者一步走两级台阶,请计算共有多少种走法。
解题思路:
-
首先考虑最后一步的情况,要么是从第九级台阶再走一级到第十级,要么是从第八级台阶走两级到第十级,也就是说,要想到达第十级台阶,最后一步一定是从第八级或者第九级台阶开始的
-
如果已知从地面到第八级台阶一共有X种走法,从地面到第九级台阶一共有Y中走法,则从地面走到第十级台阶一定是X+Y
-
假设用F(n) 表示第n级台阶的走法数量,根据以上分析,可以得到 F(10) = F(9) +F(8),推广而知,
F(n) = F(n-1) + F(n-2)
-
当问题细化,只有一级和两级台阶时,可以直接得出结论。
-
所以,该问题作为动态规划问题求解的三个要素就全部出现了
边界:F(1)=1, F(2)=2 最优子结构: F(n)的最优子结构为F(n-1)和F(n-2) 状态转移函数: F(n)=F(n-1)+F(n-2)
代码实现
def upstairs(n):
# 初始化边界值
step1 = 1
step2 = 2
temp = 0
# 判断当前台阶级数是否小于1
if n < step1:
print(0)
# 判断当前台阶级数是否为1
if n == step1:
print(1)
# 判断当前台阶级数是否为2
if n == step2:
print(2)
# 迭代求解各级台阶的走法数量
for i in range(3, n):
temp = step1 + step2
step1 = step2
step2 = temp
print(temp)
n = 10
upstairs(10)
背包问题
经典动态规划问题--背包问题
问题描述
有N个重量为w1,w2,w3,...,wn,价值为v1,v2,v3,...,vn的物品和一个承重量为W的背包,求让背包里装入的物品具有最大的价值总和的物品子集
思路解析:为了设计一个动态规划算法,需要推导出一个递推关系,用较小实例的解的形式来表示背包问题的实例解。
- 首先考虑有前 i(1<=i<=N)个物品定义的实例,物品的重量分别为 w1,w2,....,wi,价值为v1,v2,...,vi,背包目前的承重量为j(1<=j<=W)。设F(i, j)为组成该实例最优解的物品的总价值,也就是能够放进承重量为j的背包中的前i个物品中最有价值的子集的总价值。
- 可以将前 i 个物品中能够放进承重量为j 的背包中的子集分为两种类别:
- 包括第 i 个物品的子集
- 不包括第 i 个物品的子集
- 根据定义,在不包括第 i 个物品的子集中, 最优子集的价值为F(i-1, j)
- 在包括第 i 个物品子集中,最优子集是由该物品和前 i-1 个物品中能够放进承重量为 j-wi的背包的最优子集组成,其总价值等于vi + F(i-1,j-wi)
- 所以,在前i 个物品中,最优解的总价值等于以上两种情况求得的价值的较大值
- 如果第 i 个物品不能放进背包,从前 i 个物品中选出的最优子集的总价值即等于从前 i-1个物品中选出的最优子集的总价值。得到递推式与边界
问题实例
假设物品的数量为4,背包的承重量为8,各个物品的重量和价值如下表
i | 1 | 2 | 3 | 4 |
---|---|---|---|---|
w | 2 | 3 | 4 | 5 |
v | 3 | 4 | 5 | 6 |
动态规划与分治法类似,都是把大问题拆成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到原问题的目的
但分治法在子问题和子子问题上被重复计算很多次,而动态规划具有记忆性,通过记录已经解决的子问题,在新问题中需要用到子问题可以直接提取,避免重复计算,从而节约时间。
动态规划算法解决问题的核心就在于记录,找出最优解。
代码实现
def bag(n, c, w, v):
res = [ [-1 for j in range(c+1)] for i in range(n+1)]
for j in range(c+1):
res[0][j] = 0
for i in range(1, n+1):
for j in range(1, c+1):
res[i][j] = res[i-1][j]
if j >= w[i-1] and res[i][j] < res[i-1][j-w[i-1]] + v[i-1]:
res[i][j] = res[i-1][j-w[i-1]] + v[i-1]
return res
def show(n, c, w, res):
print('Max_value::', res[n][c])
x = [False for i in range(n)]
j = c
for i in range(1, n+1):
if res[i][j] > res[i-1][j]:
x[i-1] = True
j -= w[i-1]
print('The selected items are as follows:')
for i in range(n):
if x[i]:
print(f' No.{i}')
if __name__ == '__main__':
n = 5
c = 10
w = [2, 2, 6, 5, 4]
v = [6, 3, 5, 4, 6]
res = bag(n, c, w, v)
show(n, c, w, res)
最长递归子序列问题
经典动态规划问题--最长递归子序列问题
问题描述
最长递增子序列问题即给定一个序列,求解其中最长的递增子序列的长度
长度为i 的序列 Ai{a1,a2,a3,...,ai}的最长递增子序列,需要先求出序列 Ai-1{a1,a2,a3,...ai-1} 中各元素作为最大元素的最长递增序列,然后把所有这些递增序列与a1进行比较。如果某个长度为m的序列的末尾元素aj比ai小,则将元素ai加入到这个递增子序列,得到一个新的长度为m+1的新序列,否则长度不变
代码实现
def get_dp2(arr):
n = len(arr)
dp, ends = [0]*n, [0]*n
ends[0], dp[0] = arr[0], 1
right, l, r, m = 0, 0, 0, 0
for i in range(1, n):
l = 0
r = right
# 二分查找
while l <= r:
m = int((l + r)/ 2)
if arr[i] > ends[m]:
l = m + 1
else:
r = m - 1
right = max(right, l)
ends[l] = arr[i]
dp[i] = l + 1
# 根据最长递增序列的第二个数求出最小索引
dp_min_index = dp.index(min(dp) + 1) - 1
# 最大索引
dp_max_index = dp.index(max(dp))
# 切片注意左闭右开
dp2 = arr[dp_min_index: dp_max_index + 1]
return dp2
arr = [15, 3, 3, 6, 7, 8, 9, 11, 13, 1, 2, 5]
dp2 = get_dp2(arr)
print(dp2)