#include<iostream> usingnamespace std; constint N = 1010; int f[N][N]; int v[N],w[N]; intmain() { int n,m; 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]); }
这个优化可以简单的 O(N2) 地实现,一般都可以承受。另外,针对背包问题而言, 比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以 O(V+N) 地完成这个优化。
转化为01背包问题求解(可参考)
01 背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为 01 背包问题来解。
最简单的想法是,考虑到第 i 种物品最多选 ⌊V/Ci⌋ 件,于是可以把第 i 种物品转化为 ⌊V/Ci⌋ 件费用及价值均不变的物品,然后求解这个 01 背包问题。这样的做法完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为 01 背包问题的思路:将一种物品拆成多件只能选 0 件或 1 件的 01 背包中的物品。
更高效的转化方法是:把第 i 种物品拆成费用为 Ci2k 、价值为 Wi2k 的若干件物品,其中 k 取遍满足 Ci2k≤V 的非负整数。
这是二进制的思想。因为,不管最优策略选几件第 i 种物品,其件数写成二进制后, 总可以表示成若干个 2k 件物品的和。这样一来就把每种物品拆成 O(log⌊V/Ci⌋) 件物品,是一个很大的改进。
O(VN) 算法
这个算法使用一维数组,先看伪代码:
1 2 3 4
F[0..V] = 0 for i from 1 to N for v from Ci to V F[v] = max(F[v], F[v − Ci] + Wi)
这个伪代码与 01 背包问题的伪代码只有 v 的循环次序不同而已。
为什么这个算法就可行呢?首先想想为什么 01 背包中要按照 v 递减的次序来循环。 让 v 递减是为了保证第 i 次循环中的状态 F[i,v] 是由状态 F[i−1,v−Ci] 递推而来。 换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 F[i−1,v−Ci]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 i 种物品”这种策略时, 却正需要一个可能已选入第 i 种物品的子结果 F[i,v−Ci],所以就可以并且必须采用 v 递增的顺序循环。这就是这个简单的程序为何成立的道理。
值得一提的是,上面的伪代码中两层 for 循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。
#include<iostream> usingnamespace std; constint N = 1010; int f[N]; int v[N],w[N]; intmain() { int n, m; 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; }
intmain(){ 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 = 1; j <= m; j ++){//枚举体积 for(int k = 0; k <= s[i]; k ++){ if(j >= k * v[i]){ f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]); } } } }
下面介绍一种实现较为简单的 O(VN) 复杂度解多重背包问题的算法。 它的基本思想是这样的:设 F[i,j] 表示“用了前 i 种物品填满容量为 j 的背包后, 最多还剩下几个第 i 种物品可用”, 如果 F[i,j]=−1 则说明这种状态不可行, 若可行应满足 0≤F[i,j]≤Mi 。
递推求 F[i,j] 的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11
F[0, 1 . . . V ] ← −1 F[0, 0] ← 0 for i ← 1 to N for j ← 0 to V if F[i − 1][j] ≥ 0 F[i][j] = Mi else F[i][j] = −1 for j ← 0 to V − Ci if F[i][j] > 0 F[i][j + Ci] ← max{F[i][j + Ci], F[i][j] − 1}
intzeroOnePack(int n, int m, const vector<Good>& goods){ vector<int> dp(m + 1, 0); for (constauto& good : goods) { for (int j = m; j >= good.v; j--) { dp[j] = max(dp[j], dp[j - good.v] + good.w); } }
return dp[m]; }
intmain() { int t = 1, n, m, wi, vi, s; cin >> n >> m; vector<Good> goods; while(n--) { cin >> vi >> wi >> s; int k; // Break multiple knapsack into 0-1 knapsack using binary split auto binarySplitToZeroOne = [&]() { for (k = 1; k <= s; k <<= 1) { goods.push_back({k * vi, k * wi}); s -= k; } if (s > 0) { goods.push_back({s * vi, s * wi}); } }; if (s > 0) { // Multiple knapsack binarySplitToZeroOne(); } elseif (s == 0) { // Complete knapsack s = m / vi; // convert to multiple knapsack binarySplitToZeroOne(); } elseif (s == -1) { // zero one knapsack goods.push_back({vi, wi}); } }
auto res = zeroOnePack(n, m, goods); cout << res <<endl;
费用增加了一维,只需状态也增加一维即可。设 F[i,v,u] 表示前 i 件物品付出两种费用分别为 v 和 u 时可获得的最大价值。状态转移方程为:
F[i,v,u]=max{F[i−1,v,u],F[i−1,v−Ci,u−Di]+Wi}
根据01背包问题的状态压缩优化思路,可以只使用二维的数组,当每件物品只可以取一次时,变量 v 和 u 采用逆序的循环。
当物品有如完全背包问题时采用顺序的循环,当物品有如多重背包问题时拆分物品。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include<bits/stdc++.h> usingnamespace std;
int n, V, M; constint N = 1e3 + 5; int v[N], m[N], w[N], f[N][N];
intmain(){ cin >> n >> V >> M; for (int i = 1; i <= n; i ++) { cin >> v[i] >> m[i] >> w[i];//体积,重量,价值 } for (int i = 1; i <= n; i ++) for (int j = V; j >= v[i]; j --) for (int k = M; k >= m[i]; k --) f[j][k] = max (f[j - v[i]][k - m[i]] + w[i], f[j][k]); cout << f[V][M]; return0; }
物品总个数的限制
有时,“二维费用”的条件为:最多只能取 U 件物品,这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为 U 。换句话说,设 F[v,u] 表示付出费用 v 、最多选 u 件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在 F[0...V,0...U] 范围内寻找答案。
constint N = 110; int n,m; int h[N],e[N],ne[N],idx; /*h数组是邻接表的头它的下表是当前节点的标号,值是当前结点第一条边的编号(其实是最后加入的那一条边),e数组是边的集合,它的下标是当前边的编号,数值是当前边的终点; ne是nextedge,如果ne是-1表示当前结点没有下一条边,ne的下标是当前边的编号,数值是当前结点的下一条边的编号,idx用于保存每一条边的上一条边的编号。 这样我们就知道了当前结点的第一条边是几,这个边的终点是那个结点,该节点的下一条边编号是几,那么邻接表就完成了 */ int v[N],w[N],f[N][N];