背包问题
0-1背包
普通做法
每件物品要么拿,要么不拿,只有 1 件,且不许拆分,这个时候,不能用贪心。例如“采药问题”,如果输入数据为:
10 3
60 6
40 5
40 5
从单位时间的采摘价值来看,第一株草药单位时间可采价值为 10,最大,所以用贪心做就会优先选择第一株,但显然是错的,采后两株能得到的总价值为 80,更大。原因即在于这里不允许拆第二株只采 4 分钟得到 32 的价值,要么采 5 分钟得到 40,要么得到 0。
由此可以看出,贪心无法解决“0-1背包”问题,需要使用动态规划思想。我们将草药作为阶段进行划分,用 f[i][j] 表示状态:在前 \(i\) 株草药中选择一些草药,时间限制为 \(j\) 的最大总价值。通过状态转移方程来进行阶段转移:
最终要求的结果即为状态 f[N][T] 的值。
由此得到我们的第一个代码:
// f[i][j]:在前 i 株草药中选择一些草药,时间限制为 j 的最大总价值
// f[i][j] = max(f[i-1][j], f[i-1][j-time[i]]+value[i]);
#include <iostream>
#include <algorithm>
using namespace std;
const int C = 107, M = 1007;
int f[C][M];
int main() {
int T, N;
scanf("%d%d", &T, &N);
for (int i = 1, time, value; i <= N; ++i) {
scanf("%d%d", &time, &value);
for (int j = 1; j <= T; ++j) {
f[i][j] = f[i-1][j];
if (j >= time)
f[i][j] = max(f[i][j], f[i-1][j-time]+value);
}
}
printf("%d\n", f[N][T]);
}
第一次改进:滚动数组
既然已经写出代码,我们就只看代码进行优化。
由上面的循环可知,f[i][j] 的值只由其上一行产生,与其余行无关。因此,我们在循环过程中,仅保存两行内容即可。
初始,我们令 u = 1,表示原来的第 \(i\) 行,它的上一行就是 u = 0;
接着,我们通过 u ^= 1 将 u 取反,变原来的“上一行”为新的“下一行”。
i i-1 u u^1
1 0 1 0
2 1 0 1
3 2 1 0
4 3 0 1
...
最终结果即转化为:f[N&1][T]
代码转换为:
// f[u][j] = max(f[u^1][j], f[u^1][j-t[i]]+v[i]);
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1007;
int f[2][M];
int main() {
int T, N;
scanf("%d%d", &T, &N);
for (int i=1, u=1, t, v; i <= N; ++i, u^=1) {
scanf("%d%d", &t, &v);
for (int j = 1; j <= T; ++j) {
f[u][j] = f[u^1][j];
if (j >= t)
f[u][j] = max(f[u][j], f[u^1][j-t]+v);
}
}
printf("%d\n", f[N&1][T]);
}
第二次改进:降维
我们继续观察第一个代码,既然每一行都仅由上一行得出,那么我们能不能只用一行进行更新呢?
这有一个前提:如果新的一行的更新顺序是单调的(从左至右或从右至左,或从大到小或从小到大,总之有序),则可以按序将该行内容逐个更新,最终变为新的一行。
当 \(j \lt t\) 时,第 \(i\) 行的内容就是第 \(i-1\) 的内容,不需要进行更新。我们直接从 \(t\) 开始更新。从式子 f[i][j] = max(f[i-1][j], f[i-1][j-time]+value); 可以看出,第 \(j\) 列的内容是由上一行的第 \(j\) 列及第 \(j-time\) 列的旧值更新而来,所以在更新第 \(j\) 列以前,第 \(j-time\) 列应该保持原来的值,我们必须从大到小(从右到左)进行更新。
代码如下:
// f[j] = max(f[j], f[j-t[i]]+v[i]);
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1007;
int f[M];
int main() {
int T, N;
scanf("%d%d", &T, &N);
for (int i=1, t, v; i <= N; ++i) {
scanf("%d%d", &t, &v);
for (int j = T; j >= t; --j)
f[j] = max(f[j], f[j-t]+v);
}
printf("%d\n", f[T]);
}
总结一下,滚动数组仅对空间进行了优化,时间上保持不变;而降维同时优化了时间与空间。
完全背包
在背包问题中,当每种物品的数量没有上限时,我们称之为“完全背包”问题。其实不难发现,完全背包的每种物品也是有上限的(\(V/w[i]\)),其中 \(V\) 为背包的总体积,\(w[i]\) 为第 \(i\) 种物品的单件体积。
普通代码
我们直接可以根据"0-1背包"的思路,写出如下状态转移方程:
不难看出,我们需要多一层循环 \(k\):
这其实就是一个“多重背包问题”,代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 33, M = 207;
int f[N][M];
int main() {
int m, n;
scanf("%d%d", &m, &n);
for (int i=1, w, c; i <= n; ++i) {
scanf("%d%d", &w, &c);
for (int j = 1; j <= m; ++j)
for (int k = 0; k*w <= j; ++k)
f[i][j] = max(f[i][j], f[i-1][j-k*w]+k*c);
}
printf("max=%d\n", f[n][m]);
}
第一次改进:滚动数组
原理同“0-1背包”问题,滚动数组可以降低空间,但不能降低时间。
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 207;
int f[2][M];
int main() {
int m, n;
scanf("%d%d", &m, &n);
for (int i=1, u=1, w, c; i <= n; ++i, u^=1) {
scanf("%d%d", &w, &c);
for (int j = 1; j <= m; ++j)
for (int k = 0; k*w <= j; ++k)
f[u][j] = max(f[u][j], f[u^1][j-k*w]+k*c);
}
printf("max=%d\n", f[n&1][m]);
}
第二次改进:降维
我们仔细观察一下上面的代码,发现 \(k\) 循环其实有很多重复的操作。
我们将 f[i][j-w[i]]+v[i] 替代 f[i][j] 的 max 中除 f[i-1][j] 以外的其余项,得:
请注意:它和“0-1背包”不一样,max 中出现了 f[i][...],而不是 f[i-1][...]。
现在我们考虑开始降维,先考虑单调性,由于第 \(j\) 列是由第 \(j-w[i]\) 列的新值产生的,所以第 \(j\) 列更新之前,必须先更新第 \(j-w[i]\) 列,我们应该从左到右更新。
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 207;
int f[M];
int main() {
int m, n;
scanf("%d%d", &m, &n);
for (int I = 1, w, c; i <= n; ++i) {
scanf("%d%d", &w, &c);
for (int j = w; j <= m; ++j)
f[j] = max(f[j], f[j-w] + c);
}
printf("max=%d\n", f[m]);
}
多重背包
我们还有一种常见的思路——将所有的背包全部转为“0-1背包”或“多重背包”完成。其中,多重背包比较好做。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 13, M = 207;
int f[N][M];
int main() {
int V, n;
scanf("%d%d", &V, &n);
for (int i = 1, w, v, x; i <= n; ++i) {
scanf("%d%d%d", &w, &v, &x);
for (int j = 1; j <= V; ++j)
for (int k = 0; k*w<=j && k<=x; ++k)
f[i][j] = max(f[i][j], f[i-1][j-k*w]+k*v);
}
printf("%d\n", f[n][V]);
}
对于混合背包问题,我们可以将其全部转为多重背包问题。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 37, M = 207;
int f[N][M];
int main() {
int V, n;
scanf("%d%d", &V, &n);
for (int i = 1, w, v, x; i <= n; ++i) {
scanf("%d%d%d", &w, &v, &x);
if (!x) x = V / w;
for (int j = 1; j <= V; ++j)
for (int k = 0; k*w<=j && k<=x; ++k)
f[i][j] = max(f[i][j], f[i-1][j-k*w]+k*v);
}
printf("%d\n", f[n][V]);
}
更高阶的玩法
我们发现多重背包,无非就是对取值数量进行枚举的“0-1背包”,所以,如果我能将多重背包迅速地拆成尽量少的“0-1背包”,就可以进一步提高多重背包(完全背包也是)的效率。
最快的拆法当然就是用倍增(类二进制枚举),例如当物品数量为 13 时,我们除从 1 到 13 进行枚举外,还可以将其拆为 \(1, 2, 4, 6\) 共 4 种“0-1背包”,并使用“0-1背包”进行运算。
例如上题混合背包问题,使用二进制拆分枚举,得到如下代码:
#include <iostream>
const int N = 37, M = 207;
int f[M];
int main() {
int v, n;
scanf("%d%d", &v, &n);
for (int i = 1, W, C, P; i <= n; ++i) {
scanf("%d%d%d", &W, &C, &P);
if (P == 0) P = v / W;
for (int j = 1; P; j <<= 1) {
if (P >= j) P -= j;
else j = P, P = 0;
for (int k = v; k >= j*W; --k)
f[k] = std::max(f[k], f[k-j*W] + j*C);
}
}
printf("%d", f[v]);
}
此种方法通常运行速度较快,大家可以在逃亡的准备做一下对比练习。
请注意:上面两段代码,对重量循环与对个数的循环位置不同,如果在个数循环里嵌套重量循环,则不同的个数会看成不同的物品,就有可能同时选择多个不同的个数,也就是说,有可能先选了 2 个又选了 3 个,共选了 5 个;如果在重量循环里嵌套个数循环,则不同个数的物品每次只能选择一样,即要么选 1 个,要么选 2 个,……,总之不能既选 1 个又再选 2 个。
队列优化
硬币购物
直接做会导致 MLE 和 TLE,可以使用 滚动数组+队列优化 的方式完成。
容斥原理(计数类背包问题优化)
通常专门应用于计数类背包问题。还是硬币购物问题,可以先将最大量的情况预处理,再通过容斥将不合法的情况去除。
反方向的背包类型
这道题的状态应该不难写出,由于有两个不同类型的容量,显然状态应该是二维,\(f[i][j]\),关键是它的描述是什么?
回想以下普通的背包问题,降维以后的 \(f[i]\) 表示在容量 \(i\) 的限制内所能达到的最大价值。
而这道题,需要至少满足氧气和氮气的需求时的最小重量,此时的限制不是在某一个范围内,而是至少在某个值以上。
因此,\(f[i][j]\) 表示至少满足 \(i\) 升氧气 \(j\) 升氮气的条件下(越多越好),能实现的最小重量。
这会影响我们更新的方式。之前当某一物品的重量大于给定限制时,我们是无法放进背包的;现在,当某一个气罐给出的容量大于我们给定的值时,我们是吸纳的(越多越好),需要用它的重量来更新。如果一个气罐能提供的气体超过我们的最小需求,且重量更小,那我们当然欢迎。
#include <iostream>
#include <cstring>
using namespace std;
int f[30][87];
int main() {
int n, V1, V2, v1, v2, m;
scanf("%d%d%d", &V1, &V2, &n);
memset(f, 0x3F, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d%d%d", &v1, &v2, &m);
for (int j = V1; j >= 0; --j)
for (int k = V2; k >= 0; --k)
f[j][k] = min(f[j][k], f[max(0,j-v1)][max(0,k-v2)] + m);
}
printf("%d", f[V1][V2]);
return 0;
}

浙公网安备 33010602011771号