目录
一、01背包问题
题目
思路
--一件物品,要么放,要么不放,每件物品都有相对应的体积和价值,背包的容积有最大值,没有规定必须要装满,满足能装入背包的最大价值即可。首先,每件物品的体积和价值可以开两个数组然后用下标相关联,然后再开一个dp数组。dp数组是二维,第一维是物品,第二维是背包的容积。dp[i][j]表示前i个物品、背包容积为j时,背包能装的最大价值。我们在考虑当前dp[i][j]时,就要考虑当前这个物品能不能装,第一是能不能装得下,第二是装(如果选中的话就转移到 前i-1件物品,背包容积为j-w[i]的这个状态)价值大还是不装价值大。那么外循环是i,内循环j的初始值是w[i],只要比这个初始值大就可以装下,然后比较dp[i-1][j]和dp[i-1][j-w[i]] + c[i]的值,即dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i])。
--二维数组可以压缩为一维数组,因为在计算dp[i][j]的过程中,发现只使用到dp[i][j]上面的值(dp[i-1][j])和上左边的值(dp[i-1][j-w[i]] + c[i])。也就是说,一维数组中,dp[j]右边计算完后可以留给计算dp[j+1]使用,而dp[j]左边的值则是计算dp[j]时需要的,这也是为什么一维dp只能从右往左遍历。从右往左计算,右边的数据会被更新,而左边的数据是当前计算需要使用的,没有更新;如果从右到左,左边的更新了,需要使用的值被覆盖了,就自然不行了。
代码
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int main(){
int n, v;
cin >> n >> v;
vector<int> w(n + 1, 0);
vector<int> c(n + 1, 0);
vector<int> dp(v + 1, 0);
for (int i = 1; i <= n; i++){
cin >> w[i];
}
for (int i = 1; i <= n; i++){
cin >> c[i];
}
for (int i = 1; i <= n; i++){
for (int j = v; j >= w[i]; j--){
dp[j] = max(dp[j], dp[j-w[i]] + c[i]);
}
}
cout << dp[v] << endl;
return 0;
}
二、01背包问题的最优方案
题目
思路
--要将选中的每一样物品输出,这时dp必须是二维的,并且j无论是否大于w[i]都要被遍历到,必须得到一个完整状态的dp数组。
--输出每样物品肯定需要从最终状态回溯,有可能回溯到两个位置,如果第i件物品未被选中的话回溯到dp[i-1][j],如果选中的话,回溯到dp[i-1][j-w[i]]。为了知道这件物品有没有被选中,需要增加一个dp数组ch,二维数组,ch[i][j]表示第i件物品在背包容积为j时是否被选择,如果等于0表示第i件物品没有被选中,如果等于1表示第i件物品被选中。
--题目中要求相同最优解取字典序较小的解,如果将n件物品顺序遍历得到的是字典序较大的解,如果逆序遍历得到的则是字典序较小的解,所以要逆序。
代码
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, v;
cin >> n >> v;
vector<int> w(n + 1, 0);
vector<int> c(n + 1, 0);
vector<vector<int>> dp(n + 2, vector<int>(v + 1, 0));//物品逆序遍历,防止数组越界,要定个n + 2
vector<vector<int>> ch(n + 1, vector<int>(v + 1, 0));//另一个dp数组,记录是否选中物品,选中为1,没选中为0
for (int i = 1; i <= n; i++){
cin >> w[i];
}
for (int i = 1; i <= n; i++){
cin >> c[i];
}
for (int i = n; i >= 1; i--){
for (int j = 0; j <= v; j++){
if (j >= w[i] && dp[i+1][j-w[i]] + c[i] >= dp[i+1][j]){
dp[i][j] = dp[i+1][j-w[i]] + c[i];
ch[i][j] = 1;
}
else{
dp[i][j] = dp[i+1][j];
}
}
}//dp数组的处理:在只求最大值时,只让内循环j遍历>=w[i]的部分,而小于w[i]的部分没有被更新值,现要回溯处理,每个值都应该被照顾到
cout << dp[1][v] << endl;
vector<int> res;//记录选中的物品下标
int j = v;
for (int i = 1; i <= n; i++){
if (ch[i][j] == 1){
res.push_back(i);
j -= w[i];
}
}
for (int i = 0; i < (int)res.size(); i++){
cout << res[i];
if (i != (int)res.size() - 1){
cout << " ";
}
}
cout << endl;
return 0;
}
//dp数组顺序遍历物品得到的最优解是第一个物品的字典序较大的解,逆序遍历物品则是第一个物品的字典序较小的解
三、完全背包问题
题目
![](https://i-blog.csdnimg.cn/blog_migrate/315f907d2160b3cc098d46953cd839b2.png)
思路
--与01背包问题唯一区别在于每一件商品都是无限的,不是只有1件。dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + c[i]),选了第i件物品之后还可以选第i件。
--也可以转化为一维数组,dp[j] = max(dp[j], dp[j-w[i]] + c[i]),和01背包问题完全一样。只不过必须正向遍历,因为计算dp数组需要的左侧值必须实时更新,如果保留上一个状态逆向遍历的话就不能满足一件物品可以选很多次的条件了。
代码
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int main(){
int n, v;
cin >> n >> v;
vector<int> w(n + 1, 0);
vector<int> c(n + 1, 0);
vector<int> dp(v + 1, 0);
for (int i = 1; i <= n; i++){
cin >> w[i];
}
for (int i = 1; i <= n; i++){
cin >> c[i];
}
for (int i = 1; i <= n; i++){
for (int j = w[i]; j <= v; j++){
dp[j] = max(dp[j], dp[j-w[i]] + c[i]);
}
}
cout << dp[v] << endl;
return 0;
}