版权声明:转载请注明出处 https://blog.csdn.net/zhouchen1998/article/details/88761025
背包问题
- 简述
- 一个很著名的动态规划问题,也称为0-1背包问题。
- 问题描述
- 有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个物品的子集中(j-wi>=0),最优子集为该物品和前i-1个物品中能够放进承重量为j-wi的背包的最优子集组成。这种最优子集的总价值等于vi+F(i-1,j-wi)。
- 因此,在前i个物品中,最优解的总价值等于以上两种情况的最大值,这就是最优子结构了。
- 当然,如果第i个物品不能放进背包中,那么从前i个物品中选出的最优子集的总价值即等于从前i-1个物品中选出的最优子集的总价值。于是得到下面的递推式,也是状态转移函数。
- F(i,j)=max{F(i-1,j),vi+F(i-1,j-wi)},j-wi>=0
- F(i,j)=F(i-1,j),j-wi<0
- 同时可以得到边界。
- F(0,j)=0,j>=0
- F(i,0)=0,i>=0
-至此,动态规划的三要素寻找完成,我们的目标不仅仅是求F的值,还有F取值时的物品组合。
- 总体思路
- 实例分析
-
假设物品数量为5,背包承重量为10,各个物品信息如下表。
编号 重量w 价值v 2 6 2 3 6 5 5 4 4 6 -
动态规划的原理与分治法常常类似,但是分治法将子问题和子子问题反复求解多次,动态规划擅长的则是记录之前问题的解,即填表。
量 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 6, 6, 6, 6, 6, 0, 6, 0, 6] [0, 0, 0, 0, 9, 9, 9, 0, 0, 0, 9] [0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 14] [0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 14] [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15] -
可以看到,当输入为5,10的时候,最优解为15,这是正确的。
-
- 回溯求序列
- 一般,动态规划用来求最优解,利用递归构造可以很方便的找到最后的答案。
- 注意:一旦,动态规划的题目出现输入序列,那么就不是找到答案那么简单,除了正向找到最优解,构造解表,还要反向寻找最优解的构成。
- 以上面这个例子为例,最后rst[5,10]是15,这是答案,此时定义一个物品长度的序列x并使每个元素为False。
- 对表格的每一行做遍历,指定w为最高权重10.逆序遍历表格每一行,最后一行根据函数判断rst[m][w](m为遍历下标)是否等于rst[m-1][w]若相等,则代表当前行对应的物品没有选中(原理是依据状态转移函数),否则代表选中,w-=当前行对应物品的权重,x[m]置为True,按此顺序,遍历结束。
- 输出为True的对应下标,即为最优解序列。
- 在这个过程中x的取值要么为True要么为False,这就是为什么叫做0-1背包问题。
- 代码
-
def bag(i, j, w, v, out): if i == 0 and j >= 0: out[i][j] = 0 return 0 if i >= 0 and j == 0: out[i][j] = 0 return 0 if j - w[i-1] >= 0: out[i][j] = max(bag(i-1, j, w, v, out), v[i-1] + bag(i-1, j-w[i-1], w, v, out)) return out[i][j] if j - w[i-1] < 0: out[i][j] = bag(i-1, j, w, v, out) return out[i][j] def search(rst, i, j): x = [False for m in range(i+1)] for m in range(i, -1, -1): if rst[m][j] == rst[m-1][j]: # 此时代表没有选中当前 pass else: x[m] = True j -= w[m-1] for i in range(len(x)): if x[i]: print("选择了第{}个物品".format(i)) if __name__ == '__main__': # 物品数目 i = 5 # 背包容量 j = 10 # 物品信息 w = [2, 2, 6, 5, 4] v = [6, 3, 5, 4, 6] # 结果存放表 rst = [[0 for m in range(j+1)] for n in range(i+1)] # 计算结果,构造解集合 bag(5, 10, w, v, out=rst) # 输出解集合表格 for item in rst: print(item) # 回溯查找输入序列 search(rst, i, j)
-
- 补充说明
- 具体代码可以查看我的Github,欢迎Star或者Fork
- 参考书《你也能看得懂的Python算法书》,书中略微有一点不合理之处,做了修改
- 到这里,其实你已经体会到了动态规划的简约之美,当然,要注意Python是有递归深度限制的,如不是必要,建议使用循环控制