本期任务:介绍算法中关于动态规划思想的几个经典问题
一、问题描述
给定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为物品数量);最优解是背包容量限制下的最大价值。
示例对应的递归树如下,其中
代表装入编号为0的物品前,背包容量为7,当前总价值为0:
三个特征:
- 重复子问题:
- 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
- 本题中不同的物品选择方案,可能导致相同的物品价值,如递归树中的 (仅取第一件物品)与 (仅取最后两件物品)
- 无后效性:
- 最优子结构:
- 后面阶段的状态可以通过前面阶段的状态推导出。
- 本题中,每一个状态都可以通过上一轮的状态推倒而来,如 可以由 通过拿最后一个物品来达到。
综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,关于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.问题分析
相较于原问题,题目还要求输出最大价值的物品清单。解决方案,使用
的二维数组用来存储每个状态下的最大价值,求出最大价值之后在反推物品清单即可。
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]