背包问题
1. 背包问题原理
原理
- 请参考网址:背包问题(背包九讲)。
2. AcWing上的背包题目
AcWing 423. 采药
问题描述
-
问题链接:AcWing 423. 采药
分析
- 可以把时间
T
看成背包容量,采摘每个草药的时间和价值分别看成物品的体积和价值,就转化成了01背包问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1010;
int n, m; // 物品数、背包容量
int f[N];
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) {
int v, w;
cin >> v >> w;
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
AcWing 1024. 装箱问题
问题描述
-
问题链接:AcWing 1024. 装箱问题
分析
- 将箱子的体积看成背包容量,每个物品的体积同时看成体积和价值,就变成了01背包问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 20010;
int n, m; // 物品数、背包体积
int f[N];
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) {
int v;
cin >> v;
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + v);
}
cout << m - f[m] << endl;
return 0;
}
AcWing 1022. 宠物小精灵之收服
问题描述
分析
-
本题是二维费用背包问题,精灵球数量、皮卡丘初始的体力值可以看成背包的两维体积和重量;问在满足不超过背包体积且小于背包最大重量的前提下装入的物品最大数量?并输出在最大数量下,背包剩余可以装入的重量为多少?
-
注意这里不能让物品的重量等于背包所能承受的重量,因此重量在循环时需要
-1
。 -
背包剩余可以装入的重量为多少:可以从大到小遍历重量,找到等于物品最大数量最小的重量。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1010, M = 510;
int n, V1, V2;
int f[N][M];
int main() {
cin >> V1 >> V2 >> n;
for (int i = 0; i < n; i++) {
int v1, v2;
cin >> v1 >> v2;
for (int j = V1; j >= v1; j--)
for (int k = V2 - 1; k >= v2; k--)
f[j][k] = max(f[j][k], f[j - v1][k - v2] + 1);
}
cout << f[V1][V2 - 1] << ' ';
int k = V2 - 1;
while (k > 0 && f[V1][k - 1] == f[V1][V2 - 1]) k--;
cout << V2 - k << endl;
return 0;
}
AcWing 278. 数字组合
问题描述
-
问题链接:AcWing 278. 数字组合
分析
-
问题是:给我们一个容量为
m
的背包,每个物品的体积由A
给出,每个物品可以使用一次,问恰好装满背包的方案数。 -
本题就是一个01问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 10010;
int n, m;
int f[N];
int main() {
scanf("%d%d", &n, &m);
f[0] = 1; // 什么不选和为0是一种合法方案
for (int i = 0; i < n; i++) {
int v;
scanf("%d", &v);
for (int j = m; j >= v; j--)
f[j] += f[j - v];
}
printf("%d\n", f[m]);
return 0;
}
AcWing 1023. 买书
问题描述
-
问题链接:AcWing 1023. 买书
分析
-
问题是:给我们一个容量为
m
的背包,每个物品的体积是{10, 20, 50, 100}
,每个物品可以使用无限次,问恰好装满背包的方案数。 -
本题就是一个完全背包问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 1010;
int m;
int f[N];
int v[4] = {10, 20, 50, 100};
int main() {
cin >> m;
f[0] = 1;
for (int i = 0; i < 4; i++)
for (int j = v[i]; j <= m; j++)
f[j] += f[j - v[i]];
cout << f[m] << endl;
return 0;
}
AcWing 1021. 货币系统
问题描述
-
问题链接:AcWing 1021. 货币系统
分析
-
问题是:给我们一个容量为
m
的背包,每个物品可以使用无限次,问恰好装满背包的方案数。 -
本题就是一个完全背包问题。
代码
- C++
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 3010;
int n, m;
LL f[N];
int main() {
cin >> n >> m;
f[0] = 1;
for (int i = 0; i < n; i++) {
int v;
cin >> v;
for (int j = v; j <= m; j++)
f[j] += f[j - v];
}
cout << f[m] << endl;
return 0;
}
AcWing 532. 货币系统
问题描述
-
问题链接:AcWing 532. 货币系统
分析
-
我们需要找到的货币系统
b
满足如下条件:- 性质1:
a[1]、a[2]、....、a[n]
一定可以被货币系统b
的线性组合表示出来; - 性质2:
b[1]、b[2]、....、b[m]
一定不能被b
的线性组合表示;(否则是多余的) - 性质3:最优解中,
b[1]、b[2]、....、b[m]
一定时从a[1]、a[2]、....、a[n]
选择的。
- 性质1:
-
关于性质3,可以使用反证法证明,假设
b[k]
不是从a[1]、a[2]、....、a[n]
中的某个数据,由于a、b
等价,所以b[k]
一定是两个或多个a
中的数据的线性组合表示出来的,又因为a
中的元素可以由b
中的元素线性组合表示出来,因此b[k]
可以由两个或者多个b
中的元素表示,和性质2矛盾。因此假设不成立,性质3正确。 -
有了上述性质后,本题的解决就比较简单了,步骤如下:
(1)将
a
中元素排序;(2)对于
a[i]
,查看前面的元素能否表示出a[i]
,如果可以的话,说明a[i]
多余,否则a[i]
必须保留。
代码
- C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 25010;
int n;
int a[N];
int f[M];
int main() {
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
int m = a[n - 1]; // [1~m]看是否能被已考察的元素表示出来
memset(f, 0, sizeof f);
f[0] = 1;
int res = 0;
for (int i = 0; i < n; i++) {
if (!f[a[i]]) res++; // f[a[i]] == 0说明其不能被前面的元素组合出来
for (int j = a[i]; j <= m; j++)
f[j] += f[j - a[i]];
}
cout << res << endl;
}
return 0;
}
AcWing 1019. 庆功会
问题描述
-
问题链接:AcWing 1019. 庆功会
分析
-
本题就是一个多重背包问题,最常规的做法时间复杂度是 O ( N × V × s ) O(N \times V \times s) O(N×V×s),
N、V、s
分别表示物品数、背包体积、每类物品平均有几件。 -
多重背包也可以被优化成一维,因为我们枚举选几件物品,因此体积需要从大到小枚举。
代码
- C++
#include <iostream>
using namespace std;
const int N = 6010;
int n, m;
int f[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
int v, w, s;
cin >> v >> w >> s;
for (int j = m; j >= v; j--)
for (int k = 0; k <= s && k * v <= j; k++)
f[j] = max(f[j], f[j - k * v] + k * w);
}
cout << f[m] << endl;
return 0;
}
AcWing 1020. 潜水员
问题描述
-
问题链接:AcWing 1020. 潜水员
分析
- 分析如下:
-
从本题中可以总结出三类题型:
(1)体积至多是
j
的最大价值:动态规划数组f
全部初始化为0
;状态转移过程中要保证v>=0
。(2)体积恰好是
j
的最大价值:动态规划数组f
,让f[0]=0
,其他f
置为不合法(这里求最大价值,因此初始化为负无穷,但是如果让求最少物品数则应初始化为正无穷);如果让求方案数,则f[0]=1
,其余f
初始化为0
即可;状态转移过程中要保证v>=0
。(3)体积至少是
j
的最小价值:动态规划数组f
,让f[0]=0
,其他f
置为不合法(这里求最小价值,因此初始化为正无穷);状态转移过程中不需要保证v>=0
。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 22, M = 80;
int n, m, K;
int f[N][M];
int main() {
cin >> n >> m >> K;
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
while (K--) {
int v1, v2, w;
cin >> v1 >> v2 >> w;
for (int i = n; i >= 0; i--)
for (int j = m; j >= 0; j--)
f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
}
cout << f[n][m] << endl;
return 0;
}
AcWing 1013. 机器分配
问题描述
-
问题链接:AcWing 1013. 机器分配
分析
-
每个公司可以看成一个物品组,每个公司(物品组)可以有
0、1、...、m
台设备。因为最多m
件设备,因此我们从这n
个公司(物品组)中选出的物品(设备)数不能超过m
,即背包的容量为m
。 -
物品组内某件物品的体积数就是设备数,价值就是该物品组选择这么多件物品的收益。
-
这样问题就转化成了分组背包问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 11, M = 16;
int n, m;
int w[N][M];
int f[N][M];
int way[N]; // 方案: 每个公司分配了多少台设备
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> w[i][j];
for (int i = 1; i <= n; i++) // 先循环物品组
for (int j = 0; j <= m; j++) // 在循环体积
for (int k = 0; k <= j; k++) // 再循环决策: 该物品组选择哪个物品(该公司选择几台设备)
f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
cout << f[n][m] << endl;
int j = m;
for (int i = n; i; i--)
for (int k = 0; k <= j; k++)
if (f[i][j] == f[i - 1][j - k] + w[i][k]) {
way[i] = k; // 第i个物品组(公司)选择了k件物品
j -= k;
break;
}
for (int i = 1; i <= n; i++) cout << i << ' ' << way[i] << endl;
return 0;
}
AcWing 426. 开心的金明
问题描述
-
问题链接:AcWing 426. 开心的金明
分析
- 本题就是一个01背包问题。
代码
- C++
#include <iostream>
using namespace std;
const int N = 30010;
int n, m;
int f[N];
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) {
int v, w;
cin >> v >> w;
w *= v;
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
AcWing 734. 能量石
问题描述
-
问题链接:AcWing 734. 能量石
分析
-
本题中相当于要求解所有不同的吃法的最优解。所谓不同的吃法:(1)选择吃哪些;(2)按照什么样的顺序吃。
-
首先我们确定按照什么样的顺序吃?考虑两个相邻的能量石
i、i+1
:假设此时两块能量石的能量为 E i 、 E i + 1 E_i、E_{i+1} Ei、Ei+1,则先吃i
后吃i+1
可以获得的能量为 E i + E i + 1 − S i × L i + 1 E_i+E_{i+1}-S_i \times L_{i+1} Ei+Ei+1−Si×Li+1,先吃i+1
后吃i
可以获得的能量为 E i + E i + 1 − S i + 1 × L i E_i+E_{i+1}-S_{i+1} \times L_i Ei+Ei+1−Si+1×Li。 -
如果先吃
i
获得的能量更大的话,则要满足 S i × L i + 1 < = S i + 1 × L i S_i \times L_{i+1} <= S_{i+1} \times L_i Si×Li+1<=Si+1×Li,即 S i L i < = S i + 1 L i + 1 \frac{S_i}{L_i} <= \frac{S_{i+1}}{L_{i+1}} LiSi<=Li+1Si+1,因此我们可以按照 S i L i \frac{S_i}{L_i} LiSi从小到大排序,然后已该序列的顺序选择吃那些能量石,对于每个能量石可以选择吃或者不吃。 -
最优解一定是在按照上述方式排序后一个子集,因此我们只需要枚举吃那些即可,变成了01背包问题。
-
初始化动态规划数组
f
,让f[0]=0
,其余f
值是负无穷,表示不合法状态。f[i]
:用时为i
,杜达可以获得的最大能量。
代码
- C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n, m; // 能量石数,总用时
struct Stone {
int s, e, l;
bool operator< (const Stone & W) const {
return s * W.l < W.s * l;
}
} stone[N];
int f[N];
int main() {
int T;
cin >> T;
for (int C = 1; C <= T; C++) {
m = 0;
cin >> n;
for (int i = 0; i < n; i++) {
int s, e, l;
cin >> s >> e >> l;
stone[i] = {s, e, l};
m += s;
}
sort(stone, stone + n);
memset(f, -0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i < n; i++) {
int s = stone[i].s, e = stone[i].e, l = stone[i].l;
for (int j = m; j >= s; j--) // j-s时刻开始吃stone[i]
f[j] = max(f[j], f[j - s] + max(0, e - (j - s) * l));
}
int res = 0;
for (int i = 0; i <= m; i++) res = max(res, f[i]);
printf("Case #%d: %d\n", C, res);
}
return 0;
}
/**
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n, m; // 能量石数,总用时
struct Stone {
int s, e, l;
} stone[N];
int f[N];
bool cmp(Stone a, Stone b) {
return a.s * b.l < b.s * a.l;
}
int main() {
int T;
cin >> T;
for (int C = 1; C <= T; C++) {
m = 0;
cin >> n;
for (int i = 0; i < n; i++) {
int s, e, l;
cin >> s >> e >> l;
stone[i] = {s, e, l};
m += s;
}
sort(stone, stone + n, cmp);
memset(f, -0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i < n; i++) {
int s = stone[i].s, e = stone[i].e, l = stone[i].l;
for (int j = m; j >= s; j--) // j-s时刻开始吃stone[i]
f[j] = max(f[j], f[j - s] + max(0, e - (j - s) * l));
}
int res = 0;
for (int i = 0; i <= m; i++) res = max(res, f[i]);
printf("Case #%d: %d\n", C, res);
}
return 0;
}
*/
AcWing 487. 金明的预算方案
问题描述
-
问题链接:AcWing 487. 金明的预算方案
分析
-
每个主件和对应的附件构成了一个物品组,我们必须选择主件,附件可以选择任意个,如果有
k
个附件,则有 2 k 2^k 2k种选法,可以使用二进制枚举这些抉择。是一个分组背包问题。 -
本题中的价值是重要度和价格的乘积。
代码
- C++
#include <iostream>
#include <vector>
#define v first
#define w second
using namespace std;
typedef pair<int, int> PII;
const int N = 65, M = 32010;
int n, m; // 物品组、体积
PII master[N]; // 主件
vector<PII> servant[N]; // 附件
int f[M];
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
int v, w, q;
cin >> v >> w >> q;
if (!q) master[i] = {v, v * w}; // 主件
else servant[q].push_back({v, v * w});
}
for (int i = 1; i <= n; i++) // 首先枚举物品
for (int j = m; j >= 0; j--) { // 然后枚举体积
auto &sv = servant[i];
for (int k = 0; k < 1 << sv.size(); k++) { // 最后枚举决策
int v = master[i].v, w = master[i].w;
for (int u = 0; u < sv.size(); u++)
if (k >> u & 1) { // 表明选该择附件
v += sv[u].v;
w += sv[u].w;
}
if (j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
cout << f[m] << endl;
return 0;
}
3. 力扣上的背包题目
- 请参考网址:背包问题(背包九讲)。