01背包问题
参考:
https://blog.csdn.net/yandaoqiusheng/article/details/84782655
https://blog.csdn.net/qq_38410730/article/details/81667885
- 题目
有N件物品和一个容量为V的背包。第iii件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。
- 01背包的模型以及解释
Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值
递推关系式:
包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的
即V(i,j)=V(i-1,j);
还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}
。
3. 代码
#include<iostream>
using namespace std;
#include <algorithm>
int main()
{
//注意从1开始,0的位置都置零
int w[5] = { 0 , 2 , 3 , 4 , 5 }; //商品的体积2、3、4、5
int v[5] = { 0 , 3 , 4 , 5 , 6 }; //商品的价值3、4、5、6
int bagV = 8; //背包大小
int dp[5][9] = { { 0 } }; //动态规划表
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= bagV; j++) {
if (j < w[i])//该物品太大放不进去
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
//动态规划表的输出
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
cout << dp[i][j] << ' ';
}
cout << endl;
}
return 0;
}
结果即dp[4][8]就是最后的答案:能拥有的最多的价值
- 最优解回溯
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成
,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
V(i,j)=V(i-1,j)
时,说明没有选择
第i 个商品,则回到V(i-1,j)
;
V(i,j)=V(i-1,j-w(i))+v(i)
时,说明装了第i个商品
,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i))
;
一直遍历到i=0结束为止,所有解的组成都会找到。
- 代码
#include<iostream>
using namespace std;
#include <algorithm>
int w[5] = { 0 , 2 , 3 , 4 , 5 }; //商品的体积2、3、4、5
int v[5] = { 0 , 3 , 4 , 5 , 6 }; //商品的价值3、4、5、6
int bagV = 8; //背包大小
int dp[5][9] = { { 0 } }; //动态规划表
int item[5]; //最优解情况
void findMax() { //动态规划
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= bagV; j++) {
if (j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
}
void findWhat(int i, int j) { //最优解情况
if (i >= 0) {
if (dp[i][j] == dp[i - 1][j]) {
item[i] = 0;
findWhat(i - 1, j);
}
else if (j - w[i] >= 0 && dp[i][j] == dp[i - 1][j - w[i]] + v[i]) {
item[i] = 1;
findWhat(i - 1, j - w[i]);
}
}
}
void print() {
for (int i = 0; i < 5; i++) { //动态规划表输出
for (int j = 0; j < 9; j++) {
cout << dp[i][j] << ' ';
}
cout << endl;
}
cout << endl;
for (int i = 0; i < 5; i++) //最优解输出
cout << item[i] << ' ';
cout << endl;
}
int main()
{
findMax();
findWhat(4, 8);
print();
return 0;
}
- 优化空间复杂度
以上方法的时间和空间复杂度均为O(VN),其中时间复杂度已经不能再优化了,但空间复杂度却可以优化到O(N)。
先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1...N,每次算出来二维数组f[i][0...V]的所有值。那么,如果只用一个数组f[0...V],能不能保证第i次循环结束后f[j]中表示的就是我们定义的状态f[i][j]呢?f[i][j]是由f[i−1][j]和f[i−1][j−w[i]]两个子问题递推而来,能否保证在推f[i][j]时(也即在第i次主循环中推f[j]时)能够得到f[i−1][j]和f[i−1][j−w[i]]的值呢?事实上,这要求在每次主循环中我们以j=V...0的顺序推f[j],这样才能保证推f[j]时f[j−w[i]]保存的是状态f[i−1][j−w[i]]的值。至于为什么下面有详细解释。代码如下:
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 1; j--) {
if (weight[i] <= j) {
f[j] = f[j] > f[j - weight[i]] + value[i] ? f[j] : f[j - weight[i]] + value[i];
}
}
}
简化后:
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);
- 初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求"恰好装满背包"时的最优解,有的题目则并没有要求必须把背包装满。这两种问法的区别是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包
,那么在初始化时除了f[0]为0其它f[1...V]均设为−∞
,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
这样在最后判断的时候如果dp数组的值为负的话就表示背包不能完全装满
如果并没有要求必须把背包装满
,而是只希望价格尽量大,初始化时应该将f[0...V]全部设为0
。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。
如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。