背包问题
一、01背包问题
思路:背包问题最基础的思路起点,其本质就是一个组合问题,可以理解为:“在一个集合中选择符合条件的若干,通过状态的转移,得出题目所求的答案”。接下来是一些01背包的题目学习路线:
Acwing 1.01背包问题
这个最简单,没什么好说的,原理都滚瓜烂熟了,直接上模板代码:
注:滚动数组的概念就不赘述了,这个不理解的可以听y总的讲解。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n, V;
int w[N], v[N], f[N];
int main() {
cin >> n >> V;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[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]);
}
}
cout << f[V] << endl;
return 0;
}
Acwing 423.采药
相当于01背包问题换了一个比较实际的题目背景,下面是代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int T, n;
int v[N], w[N], f[N];
int main() {
cin >> T >> n;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = T; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[T] << endl;
return 0;
}
Acwing 1024.装箱问题
和经典01背包问题不一样的地方在于,所求问题由“最大价值”转化为“最小剩余量”;因此所求结果需要用初始最大体积减去f[V]即可。
代码展示
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20010;
int V, n;
int v[N], f[N];
int main() {
cin >> V;
cin >> n;
for (int i = 1; i <= n; i++) cin >> v[i];
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + v[i]);
}
}
cout << V - f[V] << endl;
return 0;
}
Acwing 426.开心的金明
本题需要改进的点在于“最大价值”为 重要程度 * 价格 的最大值,别的没有区别,以下是代码展示
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30010;
int V, n;
int price[N], value[N], f[N];
int main() {
cin >> V >> n;
for (int i = 1; i <= n; i++) cin >> price[i] >> value[i];
for (int i = 1; i <= n; i++) {
for (int j = V; j >= price[i]; j--) {
f[j] = max(f[j], f[j - price[i]] + price[i] * value[i]);
}
}
cout << f[V] << endl;
return 0;
}
二、完全背包
思路:完全背包其实可以看作多重背包的一个分支,区别就是在选择数量方面,多重背包是有限个,而完全背包是无限个。
完全背包的刷题路线:
Acwing 3.完全背包问题
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n, V;
int v[N], w[N], f[N];
int main() {
cin >> n >> V;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= V; j++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
return 0;
}
Acwing 1021.货币系统
很经典的完全背包问题与背包方案数问题的融合,两个模板套一下就好了,以下是代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 3010;
int n, m;
LL v[N], f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i];
f[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= m; j++) {
f[j] += f[j - v[i]];
}
}
cout << f[m] << endl;
return 0;
}
三、多重背包问题
思路:就像刚刚说的,所选择的有限个背包从而进行状态转移。
多重背包的刷题路线:
Acwing 4.多重背包问题
很经典的多重背包代码模板,代码展示如下:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N], f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
}
}
}
cout << f[m] << endl;
return 0;
}
Acwing 1019.庆功会
直接套模板!!!下面是代码展示:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int n, m;
int v[N], w[N], s[N], f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
}
}
}
cout << f[m] << endl;
return 0;
}
四、二位费用背包问题
思路:由于之前的背包问题外层限制条件只有一个“体积”,二维费用则是有两个限制条件,因此我们需要多嵌套一个循环。
刷题路线:
Acwing 1022.宠物小精灵之收服
正如主题所说:二维的费用问题,本题中的费用为“精灵球数量”和“皮卡丘体力值”;滚动数组的技巧依旧可以使用,并且循环遍历的时候都需要倒序遍历;对于造成的伤害这一结果的求法,可以设置一个变量存储剩余体力值,用一个while循环层层递归回到剩余体力值即可。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m, k;
int nums[N], hurt[N], f[N][N];
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= k; i++) cin >> nums[i] >> hurt[i];
for (int i = 1; i <= k; i++) {
for (int j = n; j >= nums[i]; j--) {
for (int t = m; t >= hurt[i]; t--) {
f[j][t] = max(f[j][t], f[j - nums[i]][t - hurt[i]] + 1);
}
}
}
cout << f[n][m - 1] << ' '; // m - 1是因为体力必须大于0
int last = m; // last表示剩余的体力
while (last > 0 && f[n][m - 1] == f[n][last - 1]) last--;
cout << m - last << endl;
return 0;
}
Acwing 1020.潜水员
本题和正常的二维费用背包问题的区别在于所求结果从“最大价值”转换为“最小价值”,记得初始化状态为正无穷且f[0][0]初始化为0,代码展示如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int V1, V2, n;
int v1[N], v2[N], w[N], f[N][N];
int main() {
cin >> V1 >> V2;
cin >> n;
for (int i = 1; i <= n; i++) cin >> v1[i] >> v2[i] >> w[i];
memset(f, 0x3f ,sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = V1; j >= 0; j--) {
for (int k = V2; k >= 0; k--) {
f[j][k] = min(f[j][k], f[max(0, j - v1[i])][max(0, k - v2[i])] + w[i]);
}
}
}
cout << f[V1][V2] << endl;
return 0;
}
五、分组背包问题
刷题路线:
Acwing 9.分组背包问题
模板代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int s[N], v[N][N], w[N][N], f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> s[i];
for (int j = 1; j <= s[i]; j++) cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 1; k <= s[i]; k++) {
if (j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m] << endl;
return 0;
}
Acwing 1013.机器分配 (分组背包 + 背包方案数)
这里涉及到求具体方案的问题,需要使用倒叙推导,思路理解参考注释,以下是代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20;
int n, m;
int w[N][N], g[N][N], f[N][N], way[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> g[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];
for (int k = 0; k <= j; k++) { // 枚举第i个公司所分得的电脑数量
f[i][j] = max(f[i][j], f[i - 1][j - k] + g[i][k]);
}
}
}
cout << f[n][m] << endl;
// 这里倒着推出所有公司拥有的电脑数
for (int i = n; i >= 1; i--) { // 倒叙遍历所有公司,表示“前i家公司”
for (int j = 0; j <= m; j++) { // 遍历电脑数量,表示“第i家公司拥有的电脑数量j”
if (f[i][m] == f[i - 1][m - j] + g[i][j]) {
way[i] = j;
m -= j;
break;
}
}
}
for (int i = 1; i <= n; i++) cout << i << ' ' << way[i] << endl;
return 0;
}
AcWing 487. 金明的预算方案
本题的分组背包形式不是很好看出来;是以选择附件数量的情况为组的划分形式,由于最多有两个附件,因此最多只有4种情况,使用二进制表示一下就行了。
代码展示
#include <iostream>
#include <algorithm>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 32010;
int m, n, f[N];
PII master[N]; // master[i]表示第i个主键 = {价格, 价格 * 价值}
vector<PII> servant[30]; // servant[i]表示第i个主键连接的附件 = {价格, 价格 * 价值}
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
int a, b, c;
cin >> a >> b >> c;
b *= a;
if (!c) master[i] = {a, b}; // 如果没有附件就是主键,存入master中
else servant[c].push_back({a, b}); // 如果是附件,存入servant[c]中
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--)
{
for (int k = 0; k < 1 << servant[i].size(); k++)
{ // 用二进制表示至多的4种不同情况,1表示选这个附件,0表示不选
int v = master[i].x, w = master[i].y;
for (int q = 0; q < servant[i].size(); q++) {
if (k >> q & 1)
{
v += servant[i][q].x;
w += servant[i][q].y;
}
}
// 正常的背包问题解法
if (j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
}
cout << f[m] << endl;
return 0;
}
六、背包问题的方案问题
思路:和正常背包问题的主要不同点在于状态转移方程需要累加方案数而不是比大小。
刷题路线:
Acwing 12.背包问题求具体方案
模板代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N], f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = n; i >= 1; i--) {
for (int j = 0; j <= m; j++) {
f[i][j] = f[i + 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
}
int j = m;
for (int i = 1; i <= n; i++) {
if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
cout << i << ' ';
j -= v[i];
}
}
return 0;
}
这里需要注意的是,这里输出的具体方案要求按照字典序输出,因此在正常背包解题步骤中,需要将物品倒叙遍历,这样在最后的输出方案过程中就可以正向输出。