我在这里介绍4种常见的背包问题,这里我想按易 --> 难程度从01背包,完全背包,分组背包,多重背包的顺序介绍。(封面附在最后)
一,01背包问题(后面三个背包问题的基础)
01背包问题就是从 n 个物品中选取若干个体积为 v[i] 价值为 w[i] 的物品(同一个物品不能重复选),使容量为 V 的背包的价值最大。
1,01背包二维模板代码
#include<bits/stdc++.h>
using ll = int;
using namespace std;
#define M 10005
ll dp[M][M] = { 0 };
int main() {int n, v, w, V; cin >> V >> n;for (int i = 1; i <= n; i++) {cin >> v >> w;for (int j = 1; j <= V; j++) {dp[i][j] = dp[i - 1][j];if (j >= v)dp[i][j] = max(dp[i][j], dp[i - 1][j - v] + w);}}cout << dp[n][V];return 0;
}
状态定义:我们定义 dp[i][j] 为只考虑前 i 个物品时,j 容量能装下的物品价值总和的最大值
状态转移方程:dp[i][j] = max(dp[i][j], dp[i - 1][j - v] + w)
首先我们先继承上一个状态 dp[i][j] = dp[i - 1][j] ,然后判断是否满足状态转移的条件,即是否有空间加入当前的 i 物品,然后进行状态转移,其中状态转移的基础是 dp[i - 1][j - v] ,即不包含物品 n (只包含前 n - 1 个物品),空间为 j - v 的最大价值。
2,01背包一维模板代码
#include<bits/stdc++.h>
using ll = long long int;
using namespace std;
#define M 10005
ll dp[M] = { 0 };
int main() {int V, n, w, v;cin >> V >> n;for (int i = 1; i <= n; i++) {cin >> w >> v;for (int j = V; j >= w; j--)dp[j] = max(dp[j], dp[j - w] + v);}cout << dp[V];return 0;
}
状态定义:我们定义 dp[j] 为只考虑前 i 个物品时,j 容量能装下的物品价值总和的最大值
状态转移方程:dp[j] = max(dp[j], dp[j - w] + v)
这里的一维模板代码是二维模板代码的空间优化,这里需要被理解的主要有两点,二维压缩为一维的合理性以及为什么要倒序遍历 j 。
1,二维压缩为一维的合理性
我们的最终目的是为了得出最大价值,对于二维代码来说就是 dp[n][V] ,那其他存储空间有什么用呢,从代码中我们我们不难看出我们在状态继承和状态转移中需要用到其他存储空间(dp[i][j]), 那么一维能满足这两点吗?
从状态继承来看,如果是一维,那么其实就不存在状态继承的问题,因为你在每个状态上的操作都在同一行(只有一行);
从状态转移来看,既然状态已经被继承了,那么我们就完全可以在被继承的状态上(旧状态)的基础上转移,这与二维背包的逻辑完全相同。
2,为什么要倒序遍历 j
倒序遍历的根本原因在于01背包的特性:同一个物品不能重复选。
那换句话说,是不是如果不倒序遍历 j ,也就是正序遍历 j ,就会重复选同一个物品,答案是:是的。原因在于每个状态都是从 dp[j - w] 也就是前 w 个状态继承过来的,那如果先更新前面的再更新后面的,就会发生状态转移的基础 dp[j] 不再是(旧状态)而是已经更新过的新状态(这是一定的),外在表现就是重复选同一个物品(记住后面会考)。
二,完全背包问题
完全背包问题就是从 n 种物品中每种选取若干个体积为 v[i] 价值为 w[i] 的物品(同一种物品能重复选且每种物品的数量无上限),使容量为 V 的背包的价值最大。
1,完全背包二维模板代码
#include<bits/stdc++.h>
using namespace std;
#define M 10005
int dp[M][M] = { 0 };
int main() {int n, V, v, w;cin >> n >> V;for (int i = 1; i <= n; i++) {cin >> v >> w;for (int j = 1; j <= V; j++) {dp[i][j] = dp[i - 1][j];if (j >= v)dp[i][j] = max(dp[i][j], dp[i][j - v] + w);}}cout << dp[n][V];return 0;
}
状态定义:我们定义 dp[i][j] 为只考虑前 i 种物品时,j 容量能装下的物品价值总和的最大值
状态转移方程:dp[i][j] = max(dp[i][j], dp[i][j - v] + w)
首先我们先继承上一个状态 dp[i][j] = dp[i - 1][j], 然后判断是否满足状态转移的条件,即是否有空间加入当前的 i 物品,然后进行状态转移,其中状态转移的基础是 dp[i][j - v] ,即包含物品 n (只包含前 n 种物品),空间为 j - v 的最大价值。
不难看出,完全背包模板代码与01背包模板代码的区别就在于状态转移的基础,这也取决于完全背包问题的特性:同一种物品能重复选且每种物品的数量无上限。
2,完全背包一维模板代码
#include<bits/stdc++.h>
using ll = long long int;
using namespace std;
#define M 10005
ll dp[M] = { 0 };
int main() {int n, V, w, v;cin >> n >> V;for (int i = 1; i <= n; i++) {cin >> w >> v;for (int j = w; j <= V; j++)dp[j] = max(dp[j], dp[j - w] + v);}cout << dp[V];return 0;
}
状态定义:我们定义 dp[j] 为只考虑前 i 种物品时,j 容量能装下的物品价值总和的最大值
状态转移方程:dp[j] = max(dp[j], dp[j - w] + v)
这里的一维模板代码是二维模板代码的空间优化,这里需要被理解的主要也有两点,二维压缩为一维的合理性以及为什么要正序遍历 j 。(其中二维压缩为一维的合理性与01背包问题一致,这里不再赘述,详见上文)
1,为什么要正序遍历 j
正序遍历的根本原因也在于完全背包的特性:同一种物品能重复选且每种物品的数量无上限。
(详见01背包)
三,分组背包问题
分组背包问题就是从 n 组物品中每组选取一个体积为 v[i][k] 价值为 w[i][k] 的物品(任意一组物品能且仅能入选其中一个物品),使容量为 V 的背包的价值最大。
(二维模板代码这里就不展示了,前面已经讲过二维压缩到一维的合理性和原理了,我们直接用最优的)
1,分组背包一维模板代码
/*题目描述(hdu 1712)
输入包含多个数据集。
每个数据集的第一行包含两个正整数 N 和 M,N 表示课程数量,M 表示 ACboy 可用的天数。
接下来是一个矩阵 A[i][j],(1 <= i <= N <= 100,1 <= j <= M <= 100)。
A[i][j] 表示如果 ACboy 在第 i 门课程上花费 j 天,他将获得价值为 A[i][j] 的收益。
当 N = 0 且 M = 0 时,输入结束。
*/
#define _CRT_SECURE_NO_WARNINGS 1
#include<bits/stdc++.h>
#define M 105//提交以前记得改数据
using ll = long long int;
using namespace std;int main() {int n, m;while (~scanf("%d %d", &n, &m) && n && m) {int w[M][M];int dp[M] = { 0 };int v[M];for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) {cin >> w[i][j];v[j] = j;} for (int i = 1; i <= n; i++) for (int j = m; j >= 0; j--) //总天数for (int k = 1; k <= j; k++) //付出的天数(k只是一个编号)if (j >= v[k])dp[j] = max(dp[j], dp[j - v[k]] + w[i][k]);cout << dp[m] << endl;}return 0;
}
/*样例输入
2 2
1 2
1 3
2 2
2 1
2 1
2 3
3 2 1
3 2 1
0 0
*//*样例输出
3
4
6
*/
状态定义:我们定义 dp[j] 为只考虑前 i 组物品时,j 容量能装下的物品价值总和的最大值
状态转移方程:dp[j] = max(dp[j], dp[j - v[k]] + w[i][k])
这里的一维模板代码是二维模板代码的空间优化,这里需要被理解的主要有三点:二维压缩为一维的合理性(这里不再赘述),为什么要倒序遍历 j 以及为什么要三层循环。
1,为什么要倒序遍历 j
单从这一点来看分组背包一维代码和01背包一维代码类似,那是什么导致了这个 “类似” 呢?
我们还是从两种问题的特性出发:
01背包:同一个物品不能重复选
分组背包:任意一组物品能且仅能入选其中一个物品
大家应该发现了,“一个” 是关键,那为什么这一特性会导致倒序呢?
(详见01背包)
2,为什么要三层循环
这还是取决于分组背包的特性:每组一个。
既然每组只能选一个,我们又要价值最大,那我们就要在容量的范围内尽可能选最大的,怎么选最大呢,最简单的思路就是应用选择排序的原理:打擂台(价值更大的当擂主,然后一直守擂,知道遇到更大的挑战者),事实上我们在这里也是这么做的,而这一操作又自成一层循环,加上本来的两层,2+1=3。
四,多重背包
多重背包问题就是从 n 种物品中每种选取若干个体积为 v 价值为 w 的物品(同一种物品能重复选,每种物品的数量有限),使容量为 V 的背包的价值最大。
思路1:
在展示最优代码之前我先说一种简单的方法,把多重背包问题看成是01背包问题,毕竟多重背包问题物品是有限的而且还没有选取限制,实际上01背包问题是特殊的多重背包问题(每种物品只有一个)。但是特殊情况的解法并不一定能适用于普遍情况,当每种问题的物品太多就会TLE,所以这种方法不推荐用,也就图一乐。
1,多重背包一维模板代码
#include<bits/stdc++.h>
using ll = long long int;
using namespace std;
#define M 100005
int dp[M] = { 0 };
int main() {int V, n;cin >> V >> n;for (int i = 0, v, w, s; i < n; i++) {cin >> v >> w >> s;for (int k = 1; s; s -= k, k *= 2) {//把 s 进行二进制拆分,类似于 st 表k = min(s, k);for (int j = V; j >= k * v; j--) dp[j] = max(dp[j], dp[j - k * v] + k * w);}}cout << dp[V];return 0;
}
思路2:
这个代码一看就知道我为什么要把它放到最后了,要弄懂这个代码需要一定的 st 表的基础,那这时候就有兄弟要问了,有没有不会 st 表也能理解的方法呢,有的兄弟有的,二进制大家都懂吧,任意一个数都能用二进制表示,也就是由1,2,4,8,16,32,64...这些数中的一部分组成的,那么我们还是用沿用思路1,只是在其中加一步
for (int k = 1; s; s -= k, k *= 2) {k = min(s, k);...
}
这一步其实就是把一种问题的所有物品进行整合,整合成 1 * v,2 * v,4 * v,8 * v,16 * v,32 * v,64 * v...,然后从 1 * v 这个部分开始倒序刷表(为什么倒序我就不赘述了),这样一来不管面对多大的体积 j,我们总有一种合适的组合方式适合 j 。这样一来我们只要预处理好 1 * v,2 * v,4 * v,8 * v,16 * v,32 * v,64 * v...我们就能以对数阶的复杂度进行刷表。