本期任务:介绍算法中关于动态规划思想的几个经典问题
一、问题描述
给定n种物品和一背包。物品i的重量是wi>0,其价值为vi>0,背包的容量为c。
求在背包容量限制下物品的最大价值?
输入:
n, c = 4, 7
w = [3, 5, 2, 1]
v = [9, 10, 7, 4]
输出:
20
[0, 2, 3]
二、算法思路
1. 策略选择
一个模型:
- 0-1背包问题是典型的“多阶段决策最优解”问题,每个物品决策一次(拿或者不拿),共决策n次(n为物品数量);最优解是背包容量限制下的最大价值。
示例对应的递归树如下,其中
f
(
0
,
7
,
0
)
f(0,7,0)
f(0,7,0)代表装入编号为0的物品前,背包容量为7,当前总价值为0:
三个特征:
- 重复子问题:
- 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
- 本题中不同的物品选择方案,可能导致相同的物品价值,如递归树中的 f ( 4 , 4 , 9 ) f(4,4,9) f(4,4,9)(仅取第一件物品)与 f ( 4 , 4 , 11 ) f(4,4,11) f(4,4,11)(仅取最后两件物品)
- 无后效性:
- 最优子结构:
- 后面阶段的状态可以通过前面阶段的状态推导出。
- 本题中,每一个状态都可以通过上一轮的状态推倒而来,如 f ( 4 , 1 , 20 ) f(4,1,20) f(4,1,20)可以由 f ( 3 , 2 , 16 ) f(3,2,16) f(3,2,16)通过拿最后一个物品来达到。
综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,关于0-1背包问题的回溯求解,可以参照:【算法】【回溯篇】第7节:0-1背包问题,具体实现代码文末已给出。
2. 动态规划算法思路
动态规划使用的流程:自顶向下分析问题,自底向上解决问题!
- 使用两个一维数组来保存上一轮和本轮的状态列表,元素表示当前状态下的最大价值。
- 更新过程(状态转移思路):
更新装入当前物品后对其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
dp[j + w[i]] = max(dp[j + w[i]], pre[j] + v[i]) - 状态更新过程:
初始状态:
pre=[-1,-1,-1,-1,-1,-1,-1,-1]
dp=[-1,-1,-1,-1,-1,-1,-1,-1]
编号为0的物品决策完成:
pre=[-1,-1,-1,-1,-1,-1,-1,-1]
dp=[0,-1,-1,9,-1,-1,-1,-1]
编号为1的物品决策完成:
pre=[0,-1,-1,9,-1,-1,-1,-1]
dp=[0,-1,-1,9,-1,10,-1,-1]
编号为2的物品决策完成:
pre=[0,-1,-1,9,-1,10,-1,-1]
dp=[0,-1,7,9,-1,16,-1,-1]
编号为3的物品决策完成:
pre=[0,-1,7,9,-1,16,-1,-1]
dp=[0,4,7,11,13,16,20,17]
所以,最大价值为:20
三、Python代码实现
class Package01():
def __init__(self, n, c, w, v):
self.n = n # 物品数量
self.c = c # 背包容量
self.w = w # 物品重量
self.v = v # 物品价值
def package01(self):
"""
动态规划思路(状态转移思路):使用两个一维数组来保存上一轮和本轮的状态列表
更新装入当前物品后对本行其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
dp_arr[j + self.w[i]] = max(dp_arr[j + self.w[i]], existed[j] + self.v[i])
"""
dp_arr = [-1] * (self.c + 1) # 记录当前条件下的最大价值
dp_arr[0] = 0
dp_arr[self.w[0]] = self.v[0]
for i in range(1, self.n):
pre = list(dp_arr)
for j in range(self.c + 1):
if pre[j] >= 0 and j + self.w[i] <= self.c:
dp_arr[j + self.w[i]] = max(dp_arr[j + self.w[i]],
pre[j] + self.v[i])
return max(dp_arr)
def main():
n, c = 4, 7
w = [3, 5, 2, 1]
v = [9, 10, 7, 4]
pk = Package01(n, c, w, v)
print(pk.package01())
if __name__ == '__main__':
main()
输出结果:
20
四、问题拓展
1.题目描述
给定n种物品和一背包。物品i的重量是wi>0,其价值为vi>0,背包的容量为c。
问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
2.问题分析
相较于原问题,题目还要求输出最大价值的物品清单。解决方案,使用
n
∗
(
c
+
1
)
n*(c+1)
n∗(c+1)的二维数组用来存储每个状态下的最大价值,求出最大价值之后在反推物品清单即可。
3.具体代码
class Package01():
def __init__(self, n, c, w, v):
self.n = n # 物品数量
self.c = c # 背包容量
self.w = w # 物品重量
self.v = v # 物品价值
self.res = [] # 记录最大价值对应的物品清单
self.max_v = 0 # 记录当前状态最大价值
def package01(self):
"""
动态规划思路(状态转移思路):
针对上一轮非负位置,
更新当前位置:取当前位置与上一行位置的较大者
self.dp_arr[i][j] = max(self.dp_arr[i][j], self.dp_arr[i - 1][j])
更新装入当前物品后对本行其他位置的影响:取受影响位置的元素与当前位置+物品价值的较大者
self.dp_arr[i][j + self.w[i]] = max(self.dp_arr[i][j + self.w[i]], self.dp_arr[i - 1][j] + self.v[i])
"""
self.dp_arr = [[-1] * (self.c + 1) for _ in range(self.n)] # 记录当前条件下的最大价值
self.dp_arr[0][0] = 0
self.dp_arr[0][self.w[0]] = self.v[0]
for i in range(1, self.n):
for j in range(self.c + 1):
if self.dp_arr[i - 1][j] >= 0:
self.dp_arr[i][j] = max(self.dp_arr[i][j], self.dp_arr[i - 1][j])
if j + self.w[i] <= self.c:
self.dp_arr[i][j + self.w[i]] = max(self.dp_arr[i][j + self.w[i]],
self.dp_arr[i - 1][j] + self.v[i])
self.max_v = max(self.dp_arr[-1])
print(self.max_v)
def printRes(self):
# 反推最大价值的物品清单
max_v = self.max_v
for i in range(self.n - 1, -1, -1):
if max_v - self.v[i] >= 0 and max_v - self.v[i] in self.dp_arr[i - 1]:
self.res.append(i)
max_v -= self.v[i]
print(self.res)
def main():
n, c = 4, 7
w = [3, 5, 2, 1]
v = [9, 10, 7, 4]
pk = Package01(n, c, w, v)
pk.package01()
pk.printRes()
if __name__ == '__main__':
main()
输出结果:
20
[3, 2, 0]
回溯解法:
class Package01():
def __init__(self, n, w, v):
self.n = n # 物品数量
self.w = w # 物品重量
self.v = v # 物品价值
self.max_v = 0 # 记录最大价值
self.cur_ls = [False] * n # 记录当前物品清单
self.res = [] # 记录最大价值对应的物品清单
def package01(self, cur_c, cur_v=0, index=0):
if index == self.n: # 当遍历完所有物品进行结算!!!
if self.max_v < cur_v:
self.max_v = cur_v
self.res = list(self.cur_ls)
return
if cur_c >= self.w[index]: # 背包装的下当前物品时
self.cur_ls[index] = True
self.package01(cur_c - self.w[index], cur_v + self.v[index], index + 1)
self.package01(cur_c, cur_v, index + 1) # 不装当前物品
def main():
n, c = 4, 7
w = [3, 5, 2, 1]
v = [9, 10, 7, 4]
pk = Package01(n, w, v)
pk.package01(7, 0, 0)
print(pk.max_v)
print([i for i, _ in enumerate(pk.res) if _])
if __name__ == '__main__':
main()
输出结果:
20
[3, 2, 0]