0/1背包问题--Dynamic Programming
DP问题的特征:
- 重复子问题
- 存在最优子集
背包问题属于经典的DP问题,而0/1背包问题是属于最简单的情况。
0/1的意思是每种物品只有一件,要么放入背包中,要么不放
问题定义:
有N个物品,要放入容量为W的背包中,第i件物品重量为w(i),
价值为v(i),问要怎样放才能在不超过背包容量的基础上,获得最大的价值。
算法描述:
需要用到递归的思想,定义A(i, j)为前i个物品中在容量为j的情况下所能
达到的最大价值,则A(0,j) = 0,A(i,0) = 0(i <= N and j <= W).
如果w(i) > j时,说明第i件物品不能放入背包中,价值不变,所以A(i, j) = A(i - 1, j);
如果w(i) < j时,说明第i件物品能入入背包中,那么又有两种情况,选择放入或者是
不放,如果选择不放入,那么价值还是不变,A(i, j) = A(i - 1, j);如果选择放入,
A(i, j) = v(i) + A(i - 1, j - w(i)),到底选择放入还是不放入,需要取这两种选择的最大值。
用数学表达式可以表示为:
代码
版本1:
def B(w, v, i, j): if i == 0: if w[i] <= j: return v[i] else: return 0 without_i = B(w, v, i - 1, j) if w[i] > j: return without_i else: with_i = v[i] + B(w, v, i - 1, j - w[i]) return max(without_i, with_i) if __name__ == "__main__": w = [2, 3, 4, 5, 6] v = [1, 4, 3, 6, 8] i = len(w) - 1 j = 12 print B(w, v, i, j)
版本1的实现按照算法的思路可以解决问题,但是时间复杂度为指数级,当i取大一点
值时,需要运算很久。这也是一些递归算法存在的问题,这个时候,需要用一种叫做
memorization的技术,也就是储存中间值的方法,纪录之前已经算好的子问题的值,不
用重新计算,直接拿来用就好。
版本二:
m = {} def fastB(w, v, i, j): global m try: return m[(i, j)] except KeyError: if i == 0: if w[i] <= j: m[(i, j)] = v[i] return v[i] else: m[(i, j)] = 0 return 0 without_i = fastB(w, v, i - 1, j) if w[i] > j: m[(i, j)] = without_i return without_i else: with_i = v[i] + fastB(w, v, i - 1, j - w[i]) res = max(without_i, with_i) m[(i, j)] = res return res if __name__ == "__main__": w = [2, 3, 4, 5, 6] v = [1, 4, 3, 6, 8] i = len(w) - 1 j = 12 print fastSub(m, w, i, j)
版本二,因为加入储存中间值的变量B,实际运行中,速度大大提高,时间复杂度接近
线性,用空间换时间的方法。
附加问题:
在求出最大值的同时,需要返回具体的物品最优列表
算法描述:
这个问题需要使用到上一个问题中得到的m字典,从最后一个结果往前算,范围为[n- 1,1],当m[(i, j)] > m[(i - 1, j)]时,把物品i放入最优列表中,接着j = j - w(i), 继续往前比,比完之后,最后决定第0个物品是否要加入背包,当最优列表中所有物品重量再加上第0个物品的重量不超过容量限制时,把第0个物品添加到最优列表中,否则不加入
代码:
def findMaxSub(m, w, i, j): res = [] maxWeight = j sum = 0 for index in range(i, 0, -1): if m[(index, j)] > m[(index - 1, j)]: res.append(index) j = j - w[index] for item in res: sum = sum + w[item] if maxWeight - sum >= w[0]: res.append(0) return res