背包模型
状态表示是化零为整的过程,将一些零碎的数据化为一个集合
状态计算是化整为零的过程,需要将上述得到的集合分解为具体的数据才能进行计算
01背包
问题描述
有 \(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。
第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
特征:每样物品最多只能选择一次
问题分析
\(f(i, j)\)表示从前i个物品中选,总体积不大于j的选法的最大价值
\(f(i, j) = max(f(i - 1, j), f(i - 1)(j - v_i) + w_i)\)
关于状态转移方程,不好理解的点在于选择第i个物品时能获得的最大收益\(f(i - 1, j - v_i) + w_i\). 因为我们的判断方向是先判断前一个物品,然后再判断后一个物品,但是我们说“如果选择的第i个物品,那么此时能获得的最大收益是前i-1个物品在总体积不超过\(j - v_i\)时所能获得的最大收益”,这样来看,似乎是第i个物品的选择影响到了第i-1个物品的选择,这和我们实际分析的方向是相反的。
为什么会产生这种结论?如果产生这种疑惑,说明对于问题的理解存在一点偏差。假设正在选择第i个物品,在这个局面下,假设背包总容量是m,我们需要求解的是剩余背包容量从0到m的所有情况下可能获得的最大收益值,也就是说在背包容量剩余m这个局面时对应一个收益最大值,在剩余\(m-v_j\)时也对应一个收益最大值。不论我们有没有选择第i个物品,我们都需要从第i-1个物品的局面向后推导而来,而第i-1个物品对应的局面包括剩余0容量时的最大收益,剩余1容量时的最大收益,...., 剩余m容量时的最大收益,我们要为第i个物品确定一个来时的点,而那个点就是\(f(i - 1, j - v_i)\)。
朴素版代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
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 = 0; j <= m; ++ j)
{
f[i][j] = f[i - 1][j]; // 未选择第i个物品
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 选择第i个物品,但前提是剩余容量体积大于等于第i个物品的体积
}
cout << f[n][m] << endl;
return 0;
}
优化方法
- 将二维变为一维,
f[i][j] = f[i - 1][j]
变为f[j] = f[j]
,由于是恒等式,所以直接删除即可; if (j >= v[i])
判断直接放进循环即可,for (int j = 0; j <= m; ++ j)
变为for (int j = v[i]; j <= m; ++ j)
- 但是这样做之后,
f[j] = max(f[j], f[j - v[i]] + w[i]);
并不等价于f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
,而是等价于f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
,
因为按照j从小到大的顺序,更新f[j]
的f[j - v[i]]
已经被更新了,变成了f[i][j - v[i]]
,并不是我们想要的f[i - 1][j - v[i]]
。
所以j的遍历顺序应该变为从大到小的for (int j = m; j >= v[i]; -- j)
,这样才可以保证更新f[j]
的是上一层的f[j - v[i]]
优化版代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
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];
// 初始化需要初始f[0][0~m]均为0,由于全局数组默认为0,所以省去了初始化
for (int i = 1; i <= n; ++ i)
// for (int j = v[i]; j <= m; ++ j) // 这样做不行的原因在于更新f[j]的f[j - v[i]]已经被修改了,实际上为f[i][j - v[i]]了,而非f[i - 1][j - v[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\) 种物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
特征:每样物品可选择任意次
问题分析
\(f(i, j)\)表示从前i个物品中选,总体积不大于j的选法的最大价值
\(f(i, j) = max(f(i - 1)(j - k * v_i) + k * w_i) (0 \leq k \leq \frac{j}{v_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 = 0; j <= m; ++ j)
for (int k = 0; k * v[i] <= j; ++ k)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
优化方法
按照图片中的方法我们可以得到一次优化代码
按照01背包的优化方法,我们可以得到二次优化代码
但是需要注意的是,j的遍历顺序是从小到大的,因为更新f[i][j]
的是f[i][j - v]
,而非01背包中的f[i - 1][j - v]
,也就是我们需要的f[j - v]
是这一层已经更新过的,而非上一层的
一次优化代码
#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 = 0; j <= m; ++ j)
{
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
二次优化代码
#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) // 注意和01背包的区别
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
例题
多重背包
问题描述
有 \(N\) 种物品和一个容量是 \(V\) 的背包。
第 \(i\) 种物品最多有 \(s_i\) 件,每件体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
特征:每样物品都有固定数目
问题分析
\(f(i, j)\)表示从前i个物品中选,总体积不大于j的选法的最大价值
\(f(i, j) = max(f(i - 1)(j - k * v_i) + k * w_i) (0 \leq k \leq min(s_i, \frac{j}{v_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)
for (int j = 0; j <= m; ++ j)
for (int k = 0; k <= s[i] && k * v[i] <= j; ++ k)
f[i][j] = max(f[i][j], f[i -1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
优化方法
完全背包优化方法的不可行性
多重背包问题与完全背包问题在问题分析和朴素版代码实现上都非常类似,所以第一感觉是按照完全背包问题的优化方法进行优化,但实际并不可行,以下对其不可行性进行论述
首先考虑按照完全背包的优化方法,红色框的两部分个数是相等的,都是从j-v开始到kv大于j结束,保证这的前提是完全背包问题中每个物品均可选择任意件
但是在多重背包问题中,由于物品数量是固定的,所以下图中两橙色框部分并不一定相等,如果在k(k <= s)之前kv已经>=j了,那么2式子中的绿色部分实际并不存在,那么按照完全背包的优化方法就是可行的,但是我们并不能保证这个前提。对于2式,假设绿色部分是存在的,并且已知橙色和绿色部分的最大值,因为除绿色部分外可能包含与绿色部分相等的成分,所以我们无法求出除绿色部分外的最大值,所以按照完全背包的方法是行不通的。综上所述,按照完全背包优化多重背包虽然存在可能性,但并非100%,所以不能按照之前的方法了。
二进制优化
注意到最内层的循环for (int k = 0; k <= s[i] && k * v[i] <= j; ++ k)
,我们的目的是讨论第i种物品应选择几个。如果将这第i种物品的s个一一排列开来,我们顺序遍历,对每个物品我们都需要考虑选择或是不选择,这样做的结果是TLE。那么考虑有没有更快的方法来讨论物品应选择的个数,易知,任意一个数都可以根据二进制进行表示,所以我们将这s个物品按照二进制进行拆分,分为1个、2个、4个、8个...,将他们分别看为独立的物品,考虑每个物品是否选择就可以快速地讨论第i种物品应选择几个。根据描述可知,当拆分之后,问题就转化为了一个01背包问题。
可以看出,上述的方法主要是对朴素做法中的最内层循环进行优化,复杂度由\(O(nvs)\)优化为\(O(nvlog(s))\)
优化版代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; ++ i)
{
int a, b, s;
cin >> a >> b >> s;
// 对每一组数据根据二进制将总个数进行划分
int k = 1;
while (k <= s)
{
++ cnt;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k <<= 1;
}
if (s)
{
++ cnt;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
// 到此问题已经转化为了01背包问题
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 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
特征:有多组物品,每组物品中只能选择一个
问题分析
\(f(i, j)\)表示从前i组物品中选,总体积不大于j的选法的最大价值
\(f(i, j) = max(f(i - 1)(j - v[i][k]) + w[i][k]) (0 \leq k \leq s[i])\)
上式中\(v[i][k]\)表示第i组第k个物品的体积,w表示相同物品的价值
在第i组中的物品中任意选择一个(也可能不选),找到其中的最大价值
朴素版代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++ i)
{
cin >> s[i];
for (int j = 1; j <= s[i]; ++ j)
cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; ++ i)
for (int j = 0; j <= m; ++ j)
for (int k = 0; k <= s[i]; ++ k) // 考虑选择第i组中的哪个物品
if (j >= v[i][k])
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
cout << f[n][m] << endl;
return 0;
}
优化方法
按照01背包的优化方法即可。注意j的遍历顺序,原因此前讲过,这里不再赘述
优化版代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++ i)
{
cin >> s[i];
for (int j = 1; j <= s[i]; ++ j)
cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; ++ i)
for (int j = m; j >= 0; -- j) // 注意这里的顺序
for (int k = 0; k <= s[i]; ++ k) // 考虑选择第i组中的哪个物品
if (j >= v[i][k])
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}