背包问题
01背包问题
题意:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
思路:
二维数组思想:
通过将前 i 个物品放入进容量为j 的背包中
可以使用表格表示(二维数组思想)
1、将2顺延,因为只能够装下容量为1的物品
2、容量为2,使用2个物品时,可以装下第二个物品和容量为0时的物品,比较f[1][2]
和第二个物品+f[0][0]
的价值,选择后者
3、容量为3,可以装下第二个物品和容量为1时的物品,比较f[1][3]
和第二个物品+f[1][1]
的价值,选择后者
4、容量为1,此时选择第3个物品的容量为3,装不下,顺呈f[2][1]
5、容量为3,可以装下第3个物品,此时比较f[2][3]
和f[2][0]
+第三个物品价值4, 选择前者
6、容量为5,比较f[2][5]
和第3个物品+f[2][2]
的价值,选择后者
一维数组思想:
与二维数组思想一致,我们每一次比较都是比较上一位(未加上第i个物品时,容量为j)的价值和上一位(未加上第i个物品时,容量为j - v[i]),故只需要将数组从后往前遍历就可以做到(此处讲一下为什么从后遍历:如果从前遍历的话,相当于我们使用的是加上了第i 个物品时,容量为j - v[i]的状态,但是如果从后往前遍历,我们使用前面的第i 个物品就是还未改变的【未加上第i个物品】)
代码块:
二维:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 比较【未使用i物品但使用容量为j】和【未使用i物品但使用容量为j-v[i]加上i物品的价值 】
}
}
cout << f[n][m] << endl; // 输出的是将所有容量和物品都用完的最大价值
}
一维:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for(int i = 1; i <= n; 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;
}
完全背包问题
题意:
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
思路:
因为每一个物品都有无穷多个,所以我们每一次添加时都需要判断是否继续加i这个物品
样例解析:
使用第i个物品时可以将容量为1~5的所有情况都给表示为最大值,如图
因为样例无法解释思路中的特殊情况,故将样例的数值改了一个
改后解析:
首先所有的f值状态只和第1个物品有关(状态如图)
图中步骤:
1、在可以使用第2个物品时,容量为2,此时f[2]的价值为f[0] + w[2],比原来的f[2]大,所以发生改变
2、由f[2] 到 f[4]的改变,因为在遍历到f[4]时,f[2]已经考虑到了第二个物品的影响,换而言之,就是此时的f[2]是容量为2,使用到两个物品时的最大值,由这个状态加上w[2]与原来的f[4]进行比较
3、同2
4、(可能图中看不清,即i = 2的f[2]到i = 3的f[2])因为装不下容量为3的物品,所以不需要改变状态,图中的粗线是将f的状态顺延
代码块:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
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 <= m; j ++ ) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
多重背包问题I
题意:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
思路:
朴素做法:
在01背包的基础上再加上一重循环,进行判断s[i]个该物品,上代码发现区别
代码块:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][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 ++ ) { // 第一重循环,对前 i 个物品进行放置
for(int j = 0; j <= m; j ++ ) { // 第二重循环,对 j 个空间进行放置
for(int k = 0; k <= s[i] && k * v[i] <= j; k ++ ) { // 第三重循环,对放 k 个 i 物品进行筛选
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
}
}
}
cout << f[n][m] << endl;
return 0;
}
多重背包问题II
题意:
与多重背包I的题意相同,但是数据更严格,需要使用优化过后的算法
思路:
思想:通过二进制想法,将一个拥有很多的物品分解为logn个物品进行处理,这logn个物品可以通过组合达到满足任意个该物品的数量
如果数字过大如130:
分解为1,2,4,8,16,32,64,3即可获得所有1~130所有个数的组合
有了这个思想之后,问题就再一次变成了01背包问题
代码块:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 12010, M = 2010; // N = n * logn
/* 思想:通过二进制想法,将一个拥有很多的物品分解为logn个物品
进行处理,这logn个物品可以通过组合达到满足任意个该物品的数量*/
int n, m;
int v[N], w[N];
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0; // 下标
for(int i = 0; i < n; i ++ ) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while(k <= s)
{
v[cnt] = k * a;
w[cnt ++ ] = k *b;
s -= k;
k *= 2;
}
if(s > 0) {
v[cnt] = s * a;
w[cnt ++ ] = s *b;
}
}
n = cnt;
for(int i = 0; i < n; i ++ ) { // 01背包思路
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;
}
分组背包问题
题意:
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
思路:
思想:
根据上一次的f(i)去加上该组的物品,从大容量开始装该组的物品,只装一个,直到更新所有的f(i) ,因为一组的物品只能使用一次,所以要从后往前遍历使用上一次(还未使用i组)的状态,具体下文:
样例解析:
操作:
1、该步比较上一次的f(5)【i = 1, j = 5】和上一次的f[5 - v[k]] + 第2组的w[k],可以发现比较了一次(比较次数和组内的物品个数相同),这一步比较了两个数:
第一个:i = 1操作的f[5]
第二个:i = 1操作的f[2] + 第2组的4
明显第二个> i = 1 的 f[5]
2、因为第二组中最小的体积为3, 在使用容量为2的情况下无法使用,故顺延
3、比较的是i = 2操作的f[1] + 5 和i = 2 操作的f[5],7 < 8,故顺延
总结到此可以发现,我们每一次的变化都只使用到了改组的上一次操作和该组的第k个成员,因此,每一次操作我们都只会使用 i 组的一个成员
代码块:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N]; // v,w 表示第几组的第几个物品的体积和价值 , s 表示的是组
int f[N]; // 容量为j 可放的最大价值
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
{
cin >> s[i];
for(int j = 0; j < s[i]; j ++ ) {
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i ++ ) { // 第 i 组
for(int j = m; j >= 0; j -- ) { // 容量为 j
for(int k = 0; k < s[i]; k ++ ) { // 该组第 k 个
if(v[i][k] <= j) {
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
}
cout << f[m] << endl;
return 0;
}
总结
通过比较四种背包问题,可以发现,我们需要通过不同的题目意思去使用不同的操作方法,类如:
01背包中,每个物品只有一个,我们的状态随着使用物品的增多而改变,改变方式就是通过上一次的状态(容量 j 减去该物品的容量)加上这个物品的价值和上一次的状态(容量为 j ),状态方程可以写成f[j] = max(f[j], f[j - v[i]] + w[i])
,从上一个状态变化,因此我们是从后往前遍历。
完全背包问题中,每一个物体都可以使用无限次,我们的状态就随着使用物品增多而改变的同时,还需要考虑该物品使用多次,改变方式就是通过使用的空间为 j ,找到如果多次使用该物品那么往前寻找到使用空间为j - v[i]的 f[j - v[i]] 状态加上w[i]和原来的f[j]比较,f[j] = max(f[j], f[j - v[i]] + w[i])
,因为我们是需要考虑多个该物品,因此我们是从前往后遍历。
多重背包问题中,我们需要将多重背包问题的出现s次的物品分解为几个部分(部分的要求就是可以通过不同的组合组合出1~s的任意一种组合),然后再通过01背包思想去求解。
分组背包问题中,每一组物品都只能只用一次,同时不能同时使用该组2个以上物品,那么我们需要通过上一次的状态去从后往前遍历这一次状态,才可以满足该组该物品在该状态使用次数为1次或者不适用(即顺延),可以理解为每一组状态如果都只是用了该组的一个物品,那么下一组遍历产生的 f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
,同样,由于只能使用一次,我们是从后往前遍历。