目录
01 背包问题
(其他大部分背包问题往往都可以转换成 01 背包问题进行求解)
题目
题面
【题目描述】
一个旅行者有一个最多能装 M 公斤的背包,现在有 n 件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn,求旅行者能获得最大总价值。
【输入】
第一行:两个整数,M(背包容量,M<=200)和N(物品数量,N<=30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出】
仅一行,一个数,表示最大总价值。
【输入样例】
10 4
2 1
3 3
4 5
7 9
【输出样例】
12
传送门
信息学奥赛一本通(C++版)在线评测系统http://ybt.ssoier.cn:8088/problem_show.php?pid=1267
算法
二维数组
可以用 f[i][v] 记录前 i 件物品,背包容量为 v 的最大价值,这样最后输出 f[n][m]
可以遍历所有物品,里面再枚举背包容量
可以发现,每个物品有两种选择:放或不放
这样状态转移方程为:f[i][v] = max(f[i - 1][v], f[i - 1][v - w[i]] + c[i])
这个方程的意思是:
选第 i 件物品放还是不放价值更大
其中,f[i - 1][v] 代表该物品不放,就是跟前 i - 1 件物品容量为 v 的最高价值一样
f[i - 1][v - w[i]] + c[i] 代表该物品放,因为该物品放,所以容量少了该物品的重量( - w[i]),而价值多了该物品的价值( + c[i])
最后取最大值就行了
注意,还要判断够不够放,即 w[i] <= v,否则,f[i][v] = f[i - 1][v](即只能不放)
总体思路有了,就可以上代码了
#include <iostream>
#include <algorithm>
#define N 205
using namespace std;
int m, n, w[N], c[N], f[N][N];
int main() {
cin >> m >> n;
for(int i = 1; i <= n; ++i)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; ++i)
for(int v = 1; v <= m; ++v)
if(w[i] <= v)
f[i][v] = max(f[i - 1][v], f[i - 1][v - w[i]] + c[i]);
else
f[i][v] = f[i - 1][v];
cout << f[n][m] << endl;
return 0;
}
提交一下哈
AC ! ! !
一维数组
这回我们来个降维打击呵
我们发现,每次用的都是 f[i - 1][*],所以似乎用一个一维数组就可以了,因为每次存的都是上一次的
但是这样就需要从后往前遍历了,因为每次都用 f[v - w[i]],是前面的,大家想一下,如果从前往后遍历,那么前面的是不是就已经被赋过值了,就不是以前的了
这样,状态转移方程为:f[v] = max(f[v], f[v - w[i]] + c[i])
所以这种方法虽然优化了空间,但是只要改一点就可以了
#include <iostream>
#include <algorithm>
#define N 205
using namespace std;
int m, n, w[N], c[N], f[N];
int main() {
cin >> m >> n;
for(int i = 1; i <= n; ++i)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; ++i)
for(int v = m; v > 0; --v)
if(w[i] <= v)
f[v] = max(f[v], f[v - w[i]] + c[i]);
cout << f[m] << endl;
return 0;
}
那么稍微的提交一下~
再次 AC ! ! !
完全背包问题
题目
题面
【题目描述】
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
【输入】
第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出】
仅一行,一个数,表示最大总价值。
【输入样例】
10 4
2 1
3 3
4 5
7 9
【输出样例】
max=12
传送门
信息学奥赛一本通(C++版)在线评测系统http://ybt.ssoier.cn:8088/problem_show.php?pid=1268
算法
本题目的二维数组解法、一维数组解法都和 01 背包的类似,都只是稍加改动
二维数组
01 背包问题只能放一个,而这道题可以放无限个,所以可以再在里面加一层循环,枚举该物品放 t 个,然后重量和价钱都乘以 t(即 f[i][v] = max(f[i - 1][v], f[i - 1][v - t * w[i]] + t * c[i]),注意判断也需要乘以 t:if(w[i] * t <= v),否则 break 掉)
代码如下:
#include <iostream>
#define N 205
using namespace std;
int m, n, w[N], c[N], f[N];
int main() {
cin >> m >> n;
for(int i = 1; i <= n; ++i)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; ++i)
for(int v = m; v > 0; --v)
for(int t = 1; ; ++t)
if(w[i] * t <= v)
f[v] = max(f[v], f[v - t * w[i]] + t * c[i]);
else
break;
cout << "max=" << f[m] << endl;
return 0;
}
提交一下
AC ! ! !
一维数组
首先要想一下 01 背包的一维数组解法为什么是从大到小,因为他放过的不能再放了
而这个放过的可以再放,所以只要更改一下循环次序就可以了(简单吧?(* ̄︶ ̄))
代码如下:
#include <iostream>
#include <algorithm>
#define N 205
using namespace std;
int m, n, w[N], c[N], f[N];
int main() {
cin >> m >> n;
for(int i = 1; i <= n; ++i)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; ++i)
for(int v = 1; v <= m; ++v)
if(w[i] <= v)
f[v] = max(f[v], f[v - w[i]] + c[i]);
cout << "max=" << f[m] << endl;
return 0;
}
提交一下哈~
AC too ! ! !
(一个简单有效的优化:如果一件物品的重量比另一件的物品大,而这件物品的价值又没有另一件大,直接去掉该物品即可,正确性显然)
多重背包问题
题目
题面
【题目描述】
为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。
【输入】
第一行二个数n(n≤500),m(m≤6000),其中n代表希望购买的奖品的种数,m表示拨款金额。
接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和能购买的最大数量(买0件到s件均可),其中v≤100,w≤1000,s≤10。
【输出】
一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。
【输入样例】
5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
【输出样例】
1040
传送门
信息学奥赛一本通(C++版)在线评测系统http://ybt.ssoier.cn:8088/problem_show.php?pid=1269
算法
二维数组
几乎跟完全背包的二维一模一样!
完全背包的二维是没有限制,啥时候放不下了就不放了,换一个容量(for(int t = 1;_; ++t))
这次呢,有限制,直接从 0 到 s[i],其他都不用变
for(int k = 0; k <= s[i]; ++k)
总体代码:
#include <iostream>
#include <algorithm>
#define N 6005
using namespace std;
int n, m, 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)
if(j >= k * v[i])
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
else
break;
cout << f[m] << endl;
return 0;
}
提交一下!
AC ! ! !
一维数组
emmmm······
有一个特别简单的转换成 01 背包的方法,就是把每件物品都克隆出来对应的几件(有几件就搞几件),这样不就行了吗?
但是有点费空间……
这时,我们发现二进制可以凑成任何数(最多到他们加起来),比如 1, 2, 4, 8 最大可以组成 1 + 2 + 4 + 8 = 15:
1 = 1
2 = 2
3 = 2 + 1
4 = 4
5 = 4 + 1
6 = 4 + 2
7 = 4 + 2 + 1
8 = 8
9 = 8 + 1
10 = 8 + 2
11 = 8 + 2 + 1
12 = 8 + 4
13 = 8 + 4 + 1
14 = 8 + 4 + 2
15 = 8 + 4 + 2 + 1
这样,我们可以把每件物品变成二进制数,即价格和价值都乘以该二进制数
但是就比如可以放 13 件,如果用 1, 2, 4 最可以组成 7 件,少了;那如果用 1, 2, 4, 8 最多可以组成 15 件,又多了,都不符合题意:能放 13 件,所以,让他们的和等于 13 只能用 1, 2, 4, 6
1 = 1
2 = 2
3 = 1 + 2
4 = 4
5 = 4 + 1
6 = 6
7 = 6 + 1
8 = 6 + 2
9 = 6 + 2 + 1
10 = 6 + 4
11 = 6 + 4 + 1
12 = 6 + 4 + 2
13 = 6 + 4 + 2 + 1
即必须让他们的和等于能放几件就行了:一个数最开始等于 1,然后只要还小于等于能放的个数,就新添一个价值、价格都乘以该数的物品,然后个数减该数,该数乘以 2
别忘了最后个数还剩下呢,再新添一个都乘以个数的物品就行了
这样就完成了克隆的过程:
for(int i = 1; i <= n; ++i) {
cin >> x >> y >> s;
t = 1;
while(t <= s) {
v[++n1] = t * x;
w[n1] = t * y;
s -= t;
t *= 2;
}
v[++n1] = s * x;
w[n1] = s * y;
}
有同学问了:万一不剩下了呢?那不就都是 0 了吗?
好,能想出来这个问题的同学很棒,一看就自己思考了
我们想一下:当我们最后当 01 背包的时候,这件物品的 f[j] 和 f[j - v[i]] + w[i] 就都一样了,都是不放的状态,所以这点不用考虑
(已经转换成 01 背包了,那么 01 背包就不用说了)
下面是整体代码:
#include <iostream>
#include <algorithm>
#define N 10005
using namespace std;
int v[N], w[N], f[N], n, m, x, y, s, t, n1;
int main() {
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
cin >> x >> y >> s;
t = 1;
while(t <= s) {
v[++n1] = t * x;
w[n1] = t * y;
s -= t;
t *= 2;
}
v[++n1] = s * x;
w[n1] = s * y;
}
for(int i = 1; i <= n1; ++i)
for(int j = m; j >= v[i]; --j)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
提交一下!
AC ! ! !
尾声
如果这篇超长博客对您(您的团队)有帮助的话,就帮忙点个赞,加个关注!
最后,祝您(您的团队)在 OI 的路上一路顺风!!!
┬┴┬┴┤・ω・)ノ Bye~Bye~