动态规划五个步骤:
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
0/1背包
有n种物品,每种物品只有一个,每个物品有自己的重量和自己的价值,有一个最多只能放重量为m的背包,问这个背包最多能装价值为多少的物品
暴力方法:用回溯,每个物品只有两个状态
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
动规五部曲:
1)确定dp数组的含义: dp[i][j],表示[0,i]之间的物品种任取一个,放进容量为j的背包,价值综合最大是多少.
2)递推公式
不放物品i dp[i-1][j] 当前背包容量小于物品的重量
放物品i dp[i-1][j-weight[i]]+value[i]; 当前背包容量大于等于物品的重量,背包容量减去物品i的容量加上i的价值
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
3)初始化(当前元素肯定又左上方推导而来,所以第一行和第一列都要先初始化,再考虑其他)
x轴:背包容量
y轴:物品编号
0 1 2 3 4
物品0 0 15 15 15 15
物品1 0 0 0 0 0
物品2 0 0 0 0 0
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];//初始化第一行
}
4)确定遍历顺序:先遍历物品或者先遍历背包重量都可以
先遍历物品:
for(int i =1,i<weight.size();i++){ //遍历物品
for(int j =0;j<=bagweight;j++){ // 遍历物品,从物品1开始,跳过物品0
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
先遍历背包容量:
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品,从物品1开始,跳过物品0
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
x轴:背包容量
y轴:物品编号
0 1 2 3 4
物品0 0 15 15 15 15
物品1 0 15 15 20 35
物品2 0 15 15 20 35
二维数组实现
void test_2_wei_bag_problem1() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}
滚动(一维)数组实现:把二维降成一维
由
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
可得,当前层是由上一层的数值推导出来的倒序遍历是为了保证物品i只被放入一次
两个嵌套for循环的顺序不能颠倒,因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品
//dp数组含义:dp[j] 表示为容量为j的背包所背的最大价值 例如,dp[0]表示容量为0得背包所能装的最大价值
//递推公式: dp[j - weight[i]] + value[i] 表示容量为 j - 物品i重量 的背包 加上 物品i的价值。
//初始化: dp[0]=0;非0下标初始化成最小非负数防止覆盖后面的值 ,因此统一初始化为0
//遍历顺序: 必须先遍历物品再遍历背包容量,且背包容量必须倒序(保证每个物品只被添加一次)
顺序:
dp[1]=dp[1-1]+15=15;
dp[2]=dp[2-1]+15=30;
倒序:
dp[2]=dp[2-1]+15;
dp[1]=dp[1-1+15];
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
完全背包
有n种物品,每种物品有无限个
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的,因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
//重后往前,可以重复
// 先遍历物品,在遍历背包
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) {
for(int j = weight[i]; j <= bagWeight; j++) { // 与01背包不同点,从小到大可以保证物品被添加多次
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
多重背包
有n种物品,每种物品的个数各不相同
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 2 |
物品1 | 3 | 20 | 3 |
物品2 | 4 | 30 | 2 |
展开(就是01背包):
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 1 |
物品0 | 1 | 15 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品2 | 4 | 30 | 1 |
物品2 | 4 | 30 | 1 |
#include<iostream>
#include<vector>
using namespace std;
int main() {
int bagWeight,n;
cin >> bagWeight >> n;
vector<int> weight(n, 0);
vector<int> value(n, 0);
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) cin >> weight[i];
for (int i = 0; i < n; i++) cin >> value[i];
for (int i = 0; i < n; i++) cin >> nums[i];
for (int i = 0; i < n; i++) {
while (nums[i] > 1) { // 物品数量不是一的,都展开
weight.push_back(weight[i]);
value.push_back(value[i]);
nums[i]--;
}
}
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品,注意此时的物品数量不是n
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
优化:
#include<iostream>
#include<vector>
using namespace std;
int main() {
int bagWeight,n;
cin >> bagWeight >> n;
vector<int> weight(n, 0);
vector<int> value(n, 0);
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) cin >> weight[i];
for (int i = 0; i < n; i++) cin >> value[i];
for (int i = 0; i < n; i++) cin >> nums[i];
vector<int> dp(bagWeight + 1, 0);//优化后
for(int i = 0; i < n; i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
// 以上为01背包,然后加一个遍历个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
cout << dp[bagWeight] << endl;
}