文章目录
完全背包
方案数
买书
即求不超过n元钱的方案数
优化也是跟求最值的完全背包一样
f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − 1 ∗ v [ i ] ] + ⋯ f[i][j]=f[i-1][j]+f[i-1][j-1*v[i]]+\cdots f[i][j]=f[i−1][j]+f[i−1][j−1∗v[i]]+⋯变形为 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − v [ i ] ] f[i][j]=f[i-1][j]+f[i][j-v[i]] f[i][j]=f[i−1][j]+f[i][j−v[i]]
// Problem: 买书
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/1025/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-26 09:40:55
#include <iostream>
using namespace std;
int n;
int v[] = {10, 20, 50, 100};
int f[1010];
int main(){
cin >> n;
f[0] = 1;
for(int i = 0; i < 4; i++){
for(int j = v[i]; j <= n; j++){
f[j] += f[j - v[i]];
}
}
cout << f[n];
return 0;
}
货币系统
即求恰好为m的方案数是多少
本质是完全背包求方案数,也就是跟上一个题一样
// Problem: 货币系统
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/1023/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-26 10:02:39
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
int n, m;
int v[20];
ll f[3010];
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];
return 0;
}
货币系统(NOIP)
题意
对于一组 a n a_n an来说,等价的意思就是a表示不出来的数,b也表示不出来;a表示出来的数,b都能表示出来
找出这样一组最优解 b m b_m bm,使得m尽可能的小
题解
性质:
- a1, a2, … , an一定都能被表示出来
- 最优解中,b一定都是在a中选择出来的
- b中的每一个数不能被b中其他的数表示出来
第一个性质比较显然,我现在来写一下第二个性质的证明:
反证法。假设 b i ∉ { a i } b_i\not\in \{a_i\} bi∈{ai}。因为两个数组能表示的数是相同的集合,那么显然 b i b_i bi能表示他自己,而又因为是相同的集合,那么 b i b_i bi能用 a i a_i ai表示出来。不妨设 b i = a 1 + a 2 + a 3 b_i=a_1+a_2+a_3 bi=a1+a2+a3(这里用特例说明了,至少有两个a),那么显然 b i > a 1 , a 2 , a 3 b_i>a_1,a_2,a_3 bi>a1,a2,a3,又因为两个数列是等价的,那么a每一个数肯定能用若干个b表示出来,那么说明 b i b_i bi肯定能被其他若干个数表示出来的,那么显然 b i b_i bi是矛盾的,是重复的,这时候b不是最优的,是矛盾的,所以得证。
根据上面的三个性质,我们可以将a从小到大排序,因为a中小的数肯定不能被a中大的数表示出来
所以为了去除不是最优解的a,应当从小到大排序
还是为了去除不是最优解的a,我们考虑当前每一个a是否能被前面的a表示出来,如果能被表示出来,显然这个a是多余的,就不能选了;如果不能被表示出来,那么这个数就必选,因为这个数要是不选后面的更加不能表示它了,那么就不能满足等价关系了
可以把每个a看做体积为a,价值为a的物品,每个物品有无限个,就转换为了完全背包问题,看看前面的能不能表示这个数,就是判断装满容量是 a i a_i ai的背包的方案数是多少,看看这个是不是0,这就跟上一个题很像了。
// Problem: 货币系统
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/534/
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-27 21:30:38
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int T;
int n;
int a[110];
int f[25010];
int main(){
cin >> T;
while(T--){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
sort(a + 1, a + 1 + n);
memset(f, 0, sizeof(f));
f[0] = 1;
int m = a[n];
int ans = 0;
for(int i = 1; i <= n; i++){
if(!f[a[i]]) ans++; // 看前i - 1个数有没有表示成功这个数
for(int j = a[i]; j <= m; j++){
f[j] += f[j - a[i]];
}
}
cout << ans << endl;
}
return 0;
}
多重背包
庆功会
多重板子
#include <iostream>
using namespace std;
int n, m;
int f[6010];
int v[510];
int w[510];
int s[510];
// 时间复杂度:500 * 6000 * 10 = 3 * 10^7
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 >= v[i]; j--){
for(int k = 0; k <= s[i] && k * v[i] <= j; k++){
f[j] = max(f[j], f[j - k * v[i]] + w[i] * k);
}
}
}
cout << f[m];
return 0;
}
二维费用背包
板子
#include <iostream>
using namespace std;
const int N = 1010;
int n, m1, m2;
int v1[N];
int v2[N];
int w[N];
int f[110][110];
int main(){
cin >> n >> m1 >> m2;
for(int i = 1; i <= n; i++){
cin >> v1[i] >> v2[i] >> w[i];
}
for(int i = 1; i <= n; i++){
for(int j = m1; j >= v1[i]; j--){
for(int k = m2; k >= v2[i]; k--){
f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + w[i]);
}
}
}
cout << f[m1][m2];
return 0;
}
潜水员
参考:https://www.acwing.com/solution/content/7438/
状态表示
所有前 i 个物品中选,且氧气含量至少是 j ,氮气含量至少是 k的所有选法
属性
最小值
状态计算
包含 i 与不包含 i 的选法
跟普通的背包是类似的,即考虑 f [ i ] [ j ] [ k ] = m i n ( f [ i − 1 ] [ j ] [ k ] , f [ i − 1 ] [ j − v 1 i ] [ k − v 2 i ] ) f[i][j][k]=min(f[i-1][j][k], f[i-1][j-v1_i][k-v2_i]) f[i][j][k]=min(f[i−1][j][k],f[i−1][j−v1i][k−v2i]),但是我们注意,这里的状态表示和原来的表示是不一样的,这里是至少,答案显然不大一样
原来的二维背包转移方程是:
for(int j = m1; j >= v1; j--)
for(int k = m2; k >= v2; k--)
f[j][k] = max(f[j][k], f[j - v1][k - v2] + w[i])
这个题的转移方程如下:
for(int j = m1; j >= 0; j--)
for(int k = m2; k >= 0; k--)
f[j][k] = max(f[j][k], f[max(0, j - v1)][max(0, k - v2)] + w[i])
为什么下面的可以遍历到 0 呢?这里就要从状态表示的定义出发了。我们再来回顾一遍我们的定义:所有前 i 个物品中选,且氧气含量至少是 j ,氮气含量至少是 k的所有选法。注意,至少!比如我们现在就单拿一个体积变量来说,f[3][5]表示的是至少需要3个体积,5个重量,那么我们如果放一个比它大的物品,显然也是符合表述的,但是方程这时候就变成了有负数下标了,如果是负数也没关系,显然不存在至少选负数的情况,但是大于j的情况也会包含到j里面去,所以选负数的情况不就相当于一个都不选的情况
而为什么普通的01背包为什么只循环到v[i]呢?这时候我们也要回看我们的状态表示了:用 f [ i , j ] f[i, j] f[i,j]表示所有只从前i个物品中选,且总体积不超过j的所有方案的集合。如果j小于 v[i] 的时候,也就是下标为负数的时候,显然是不满足状态定义的,因为不可能不选这个物品还能使得总体积不超过这个负数
Code
// Problem: 潜水员
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/description/1022/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-25 16:49:41
#include <iostream>
using namespace std;
int n, m1, m2;
int v1[1010];
int v2[1010];
int w[1010];
int f[110][110];
int main(){
cin >> m1 >> m2; // o2 n2
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 = m1; j >= 0; j--){
for(int k = m2; 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[m1][m2];
return 0;
}
2021山东省赛 Adventurer’s Guild
其实看题意很明显的二维费用背包板子,但是我们还是要分析一下他的状态表示:
题意
有H点生命和S点体力,杀死一个怪物会消耗一定的生命和体力,会得到一定的报酬
生命值降为0会死,体力降为0后,还可以把多减的体力减到生命中去
题解
状态表示
f[i][j][k]表示从前 i 个怪物中选,消耗 j 点生命,k点体力获得的价值
属性
MAX
状态计算
注意
如果存好每一个属性,必须三维压二维;价值每个最多到1e9,需要开 ll
Code
// Problem: Adventurer's Guild
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/15600/H
// Memory Limit: 524288 MB
// Time Limit: 2000 ms
// Code by: ING__
//
// Powered by CP Editor (https://cpeditor.org)
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
ll n, H, S;
ll h[1010];
ll s[1010];
ll w[1010];
ll f[310][310];
int main() {
cin >> n >> H >> S;
for(int i = 1; i <= n; i++) {
scanf("%lld%lld%lld", h + i, s + i, w + i);
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = H; j > h[i]; j--) { // 不能取等于,题目要求她活着
for (int k = S; k >= 0; k--) {
if (k >= s[i]) {
f[j][k] = max(f[j][k], f[j - h[i]][k - s[i]] + w[i]);
}
else {
if (j - h[i] - (s[i] - k) > 0) // 得可以杀才能杀,前面的不用判断能不能杀,因为j的循环保证了
{
f[j][k] = max(f[j][k], f[j - h[i] - (s[i] - k)][0] + w[i]);
}
}
ans = max(ans, f[j][k]);
}
}
}
cout << ans << endl;
return 0;
}
分组背包问题
每组物品有若干个,同一组物品内最多只能选一个
机器分配
这题一开始想的时候还没大看懂哪一部分应该看做背包的物品。
即我们把每个公司看做一个物品组
第 i 个公司体积就是分配 j 台,获得的价值就是 w[i][j] 台
// Problem: 机器分配
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/1015/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-26 17:49:35
#include <iostream>
using namespace std;
const int N = 20;
int n, m;
int w[N][N];
int f[N][N];
int way[N]; // 从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 <= m; k++){ // 完全给以前讲过的分组背包板子一样,当然你可以优化不枚举到m
if(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;
j -= k;
break;
}
}
}
for(int i = 1; i <= n; i++){
cout << i << ' ' << way[i] << endl;
}
return 0;
}
金明的预算方案
这里我们要好好读一下题意:
- 每个物品都有从属关系,可能是主件,也可能是某个主件的附件。
- 不能单独选附件
- 价值是v * p
所以这就是一个分组背包问题
但是,在主从件的关系也是一个约束。无论我们做买哪一个都得买他所属的主件,那么,我们可以将问题转化为当我们知道每个主件的附件都有什么,我们选好这个一定要选的主件,然后枚举附件买的组合的可能性,即 2 i 2^i 2i可能性,最多32种,将每种方案打包,就是分组背包中的一组物品中的其中一个了
// Problem: 金明的预算方案
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/489/
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// Code by: ING__
//
// Edited on 2021-07-26 20:22:20
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define ll long long
#define re return
#define Endl "\n"
#define endl "\n"
#define v first
#define w second
using namespace std;
typedef pair<int, int> PII;
const int M = 32010;
const int N = 66;
int n, m;
vector<PII> cong[N];
PII zhu[N];
int f[M];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++){
int v, p, q;
cin >> v >> p >> q;
if(q == 0){
zhu[i] = {v, v * p};
}
else{
cong[q].push_back({v, v * p});
}
}
for(int i = 1; i <= n; i++){
if(zhu[i].v){
for(int j = m; j >= 0; j--){
for(int k = 0; k < (1 << (int)cong[i].size()); k ++){ // 二进制枚举
int v = zhu[i].v;
int w = zhu[i].w;
for(int u = 0; u < (int)cong[i].size(); u++){ // 看位数
if((k >> u) & 1){
v += cong[i][u].v;
w += cong[i][u].w;
}
}
if(j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
}
}
cout << f[m];
return 0;
}