背包问题
------------------- 0-1背包 -------------------
问题描述:
N件物品和容量为V的背包;每种物品均只有一件,且第i件物品重量为weight[i],价值为value[i]。求将哪些物品放入背包可使物品重量总和不超过背包容量,且价值总和达到最大?
解题思路:
首先定义问题状态,DP[i][V]表示前i件物品放入容量为V的背包所能获得的最大值(以下分析都如此)。
若只考虑第i件物品的放入策略,即放入或不放入两种情况,问题就可转化为一个只涉及前i-1件物品的问题,得状态转移方程:
DP[i][v] = max{ DP[i-1][v], DP[i-1][v-weight[i]] + value[i] }
空间复杂度优化:
用一维数组存储DP值,用DP[0,1,...V]表示,DP[v]表示把前i件物品放入容量为v的背包所得到的最大价值,这样可将空间复杂度从O(VN)降低到O(V)。
那么就要解决从二维转成一维过程是否合理的问题:怎样才能在DP[v]表示当前状态(即前i件物品放入容量为v的背包中所得价值)的同时,使得DP[v]和DP[v-weight[i]]分别都能标记前一状态(即前i-1件物品分别放入容量为v和v-weight[i]的背包所得价值)?
解决方法:时间复杂度O(VN)不变,仍为两重for循环,从下图可以看出,当外层为i循环,内层为v循环时,DP[0,1,...V]由于不断覆盖旧值的原因,将始终保存的是蓝色区域下边缘的系列值,对比观察改进前后的两个状态转移方程,可知此处DP[v]的值应采用逆序保存。
伪代码如下:
for i = 1,2...N for v = V...1,0 DP[v] = max{DP[v], DP[v-weight[i]] + value[i]};
------------------- 完全背包 -------------------
问题描述:
N件物品和容量为V的背包;每件物品无限可用,且第i件物品重量为weight[i],价值为value[i]。求将哪些物品放入背包可使物品重量总和不超过背包容量,且价值总和达到最大?
解题思路:
按照分析01背包思路,同样根据每件物品的放入策略,有很多种情况(不是无限种,有 0 <= k*value[i] <= v 约束条件),可得状态转移方程:
DP[i][v] = max{ DP[i-1][v], DP[i-1][v-k*weight[i]] + k*value[i] }
空间复杂度优化:
同0-1背包优化方法,DP的值仍采用一维存储,唯一区别在于DP[v]值改为顺序保存。
这就产生了疑问:完全背包中物品的放入策略不再是两种,而是多种情况,当DP用二维存储时,程序为三重for循环,那么优化后采用一维存储为什么没有了关于k的循环?
原因分析:在这里引用背包系列之完全背包中相关解释:
首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。
由此可见,二维存储转成一维存储之前,先要在思维上过渡:
DP[i][v] = max{DP[i-1][v], DP[i-1][v-k*weight[i]] + k*value[i]} || \ / \/ DP[i][v] = max{DP[i-1][v], DP[i][v-weight[i]] + value[i]} || \ / \/ DP[v] = max{DP[v], DP[v-weight[i]] + value[i]};
伪代码如下:
for i = 1,2...N for v = 0,1...V DP[v] = max{DP[v], DP[v-weight[i]] + value[i]};
------------------- 多重背包 -------------------
问题描述:
N件物品和容量为V的背包;第i件最多有num[i]件可用,且第i件物品重量为weight[i],价值为value[i]。求将哪些物品放入背包可使物品重量总和不超过背包容量,且价值总和达到最大?
代码如下:
void ZeroOnePack(int v, int w) { for(int j = V; j >= w; j--) DP[j] = max(DP[j], DP[j-w]+v); } void CompletePack(int v, int w) { for(int j = w; j <= V; j++) DP[j] = max(DP[j], DP[j-w]+v); } void MultiplePack(int v, int w, int n) { if(w*n >= V) { CompletePack(v, w); // 纯粹的完全背包 return ; } int k = 1; while(k < n) { ZeroOnePack(k*v, k*w); // 二进制思想分解 n = n - k; k = k * 2; } ZeroOnePack(n*v, n*w); }
/************************************************/ // 外层的物品种类循环在main函数中 for(i = 1; i <= N; i++) { // 内层的容量循环在MultiplePack中讨论处理 MultiplePack(v[i], w[i], num[i]); }