DP模型——背包模型 02 (完全、多重、二维费用、分组)

完全背包

方案数

买书

即求不超过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[i1][j]+f[i1][j1v[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[i1][j]+f[i][jv[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[i1][j][k],f[i1][jv1i][kv2i]),但是我们注意,这里的状态表示和原来的表示是不一样的,这里是至少,答案显然不大一样

原来的二维背包转移方程是:

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值